feat: rewrite profile section, some minor chaanges #120

Merged
alekswilc merged 2 commits from new-profiles into main 2025-04-21 20:47:12 +02:00
10 changed files with 374 additions and 269 deletions

View File

@ -6,7 +6,7 @@
"scripts": {
"build": "docker build --progress=plain -t simrailpro:backend .",
"rawbuild": "yarn tsc",
"start": "yarn build && doppler run node ../../dist/backend/index.js"
"start": "yarn rawbuild && node --env-file=.env dist/index.js"
},
"author": "Aleksander <alekswilc> Wilczyński",
"license": "AGPL-3.0-only",

View File

@ -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());

View File

@ -137,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;
};

View File

@ -16,30 +16,34 @@
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"
@ -51,17 +55,12 @@ export const Paginator = ({ page, setPage, pages }: {
</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>
</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)}>

View File

@ -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,56 +24,86 @@ 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");
export const ProfileCard = ({ data, images }: { data: TProfileData, images: TImagesData }) => {
const [sortTrainsBy, setSortTrainsBy] = useState(0);
const [sortTrainsBy2, setSortTrainsBy2] = useState(2);
const [sortStationsBy, setSortStationsBy] = useState(0);
const [hideLeaderboardStatsModal, setHideLeaderboardStatsModal] = useState(false);
const [hideProfileModal, setHideProfileModal] = useState(false);
const { isAdmin, token } = useAuth();
const adminToggleHideLeaderboardPlayerProfile = () =>
{
// #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)
{
.then((response) => {
if (response.code === 200) {
toast.success(t("admin.hideLeaderboard.alert"));
}
});
};
const adminHidePlayerProfile = () =>
{
const adminHidePlayerProfile = () => {
post(`/admin/profile/${data.player.id}/hide`, {}, { "X-Auth-Token": token })
.then((response) =>
{
if (response.code === 200)
{
.then((response) => {
if (response.code === 200) {
toast.success(t("admin.hide.alert"));
}
});
};
const adminForceUpdate = () =>
{
const adminForceUpdate = () => {
post(`/admin/profile/${data.player.id}/forceUpdate`, {}, { "X-Auth-Token": token })
.then((response) =>
{
if (response.code === 200)
{
.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), 8)];
const [dispatcherPage, setDispatcherPage] = useState(1);
const trainStats = [...chunk(Object.keys(data.player.trainStats), 8)];
const [trainPage, setTrainPage] = useState(1);
const { t } = useTranslation();
return <>
<ConfirmModal showModal={hideLeaderboardStatsModal} setShowModal={setHideLeaderboardStatsModal}
onConfirm={adminToggleHideLeaderboardPlayerProfile}
@ -84,7 +113,7 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
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">
@ -129,84 +158,52 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
</div>}
</div>
{ Object.keys(data.player.trainStats || {}).length > 0 &&
<div className="bg-white px-5 pt-6 pb-5 shadow-default dark:bg-boxdark sm:px-7.5">
<div className="group relative cursor-pointer" onClick={ () => setShowTrains(val => !val) }>
<h1 className="text-xl text-black dark:text-white pb-5">{ t("profile.trains.header") }</h1>
<ArrowIcon rotated={ showTrains }/>
</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) }>
<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>
<ArrowIcon rotated={ showStations }/>
<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>
{ showStations &&
<div className="grid grid-cols-1 gap-7.5 sm:grid-cols-3 xl:grid-cols-4 pt-4">
{dispatcherStats[dispatcherPage - 1].sort(sortStations).map(stationName => {
const station = data.player.dispatcherStats[stationName];
return <StationStat stationName={stationName} time={station.time} image={images.stations[stationName]} />
})}
</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].sort(sortTrains).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]} />
})}
</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">
@ -221,8 +218,7 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
</h5>
</div>
</div>
{ Object.keys(data.player.dispatcherStats).sort((a, b) => data.player.dispatcherStats[ b ].time - data.player.dispatcherStats[ a ].time).map(stationName =>
{
{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`}
@ -240,13 +236,11 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
</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">
<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">
@ -274,7 +268,7 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
</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") })}
</h1>

View File

@ -63,14 +63,27 @@
"trains": {
"header": "Statistiky vlaků",
"train": "Vlak",
"distance": "Vzdálenost: {{distance}}km",
"score": "Body: {{score}",
"time": "Čas: {{time}}",
"sortby": {
"title": "Řadit podle: ",
"min": "Nejnižší",
"max": "Nejvyšší",
"time": "Čas",
"distance": "Vzdálenost",
"points": "Body",
"time": "Čas"
"score": "Body"
}
},
"stations": {
"header": "Statistiky stanic",
"station": "Stanice",
"time": "Čas"
"time": "Čas: {{time}}",
"sortby": {
"title": "Řadit podle: ",
"min": "Nejnižší",
"max": "Nejvyšší"
}
},
"errors": {
"notfound": {

View File

@ -63,14 +63,27 @@
"trains": {
"header": "Train Statistics",
"train": "Train",
"distance": "Distance: {{distance}}km",
"score": "Points: {{score}}",
"time": "Time: {{time}}",
"sortby": {
"title": "Sort: ",
"min": "Lowest",
"max": "Highest",
"time": "Time",
"distance": "Distance",
"points": "Points",
"time": "Time"
"score": "Points"
}
},
"stations": {
"header": "Station Statistics",
"station": "Station",
"time": "Time"
"time": "Time: {{time}}",
"sortby": {
"title": "Sort: ",
"min": "Lowest",
"max": "Highest"
}
},
"errors": {
"notfound": {

View File

@ -63,14 +63,27 @@
"trains": {
"header": "Statystyki pociągów",
"train": "Pociąg",
"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",
"points": "Punkty",
"time": "Czas"
"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": {

View File

@ -27,19 +27,19 @@ 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 images = useSWR(`/images/`, get);
const { t } = useTranslation();
return (
<>
{/* LOADING */}
{ isLoading && <ContentLoader/> }
{(isLoading || images.isLoading) && <ContentLoader />}
{/* ERROR */}
{ error && <LoadError/> }
{(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." />}
@ -52,11 +52,11 @@ export const Profile = () =>
description={t("profile.errors.notfound.description")} />}
{/* SUCCESS */}
{ data && data.code === 200 && <PageMeta image={ data.data.player.username }
{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 && <ProfileCard data={ data.data }/> }
{data && data.code === 200 && images.data && images.data.code === 200 && <ProfileCard data={data.data} images={images.data.data} />}
</>
);
};

View File

@ -14,6 +14,6 @@
* See LICENSE for more.
*/
export const get = (url: string) => fetch(`${ import.meta.env.VITE_API_URL }${ url }`, { signal: AbortSignal.timeout(2500) }).then((res) => res.json());
export const get = (url: string) => fetch(`${ import.meta.env.VITE_API_URL }${ url }`, { signal: AbortSignal.timeout(10000) }).then((res) => res.json());
export const post = (url: string, body?: any, headers: Record<string, string> = {}) => fetch(`${ import.meta.env.VITE_API_URL }${ url }`, { signal: AbortSignal.timeout(2500), method: "POST", body: JSON.stringify(body), headers: Object.assign(headers, { "Content-Type": "application/json" }) }).then((res) => res.json());
export const post = (url: string, body?: any, headers: Record<string, string> = {}) => fetch(`${ import.meta.env.VITE_API_URL }${ url }`, { signal: AbortSignal.timeout(10000), method: "POST", body: JSON.stringify(body), headers: Object.assign(headers, { "Content-Type": "application/json" }) }).then((res) => res.json());