Merge pull request 'feat: rewrite profile section, some minor chaanges' (#120) from new-profiles into main
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #120 Reviewed-by: Aleksander Wilczyński <aleks@alekswilc.dev>
This commit is contained in:
commit
1e01260e82
@ -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",
|
||||
|
@ -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());
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -16,34 +16,38 @@
|
||||
|
||||
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">
|
||||
<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"
|
||||
@ -51,20 +55,15 @@ 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>
|
||||
<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">
|
||||
<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"
|
||||
|
@ -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,78 +24,108 @@ 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(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 = () =>
|
||||
{
|
||||
post(`/admin/profile/${ data.player.id }/${ data.player.flags.includes("leaderboard_hidden") ? "showLeaderboard" : "hideLeaderboard" }`, {}, { "X-Auth-Token": token })
|
||||
.then((response) =>
|
||||
{
|
||||
if (response.code === 200)
|
||||
{
|
||||
// #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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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), 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 }
|
||||
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">
|
||||
<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
|
||||
@ -104,179 +133,144 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
||||
<div
|
||||
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">
|
||||
<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" &&
|
||||
{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> }
|
||||
<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" &&
|
||||
{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> }
|
||||
<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].sort(sortStations).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]} />
|
||||
})}
|
||||
</div>
|
||||
<div className="flex flex-col pt-4">
|
||||
<Paginator setPage={setDispatcherPage} page={dispatcherPage} pages={dispatcherStats.length} />
|
||||
</div>
|
||||
</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 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="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 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-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 className="flex flex-col pt-4">
|
||||
<Paginator setPage={setTrainPage} page={trainPage} pages={trainStats.length} />
|
||||
</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 ];
|
||||
{/* <div className="bg-white px-5 pt-6 pb-5 shadow-default dark:bg-boxdark sm:px-7.5">
|
||||
|
||||
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>
|
||||
{ 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") }
|
||||
{t("profile.stations.station")}
|
||||
</h5>
|
||||
</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") }
|
||||
{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 ];
|
||||
{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 }
|
||||
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 }
|
||||
{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>
|
||||
<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") }
|
||||
{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 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>
|
||||
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -27,36 +27,36 @@ 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} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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());
|
Loading…
x
Reference in New Issue
Block a user