feat: rewrite profile section, some minor chaanges #120
@ -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,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)}>
|
||||
|
@ -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>
|
||||
|
@ -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,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} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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