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": {
|
"scripts": {
|
||||||
"build": "docker build --progress=plain -t simrailpro:backend .",
|
"build": "docker build --progress=plain -t simrailpro:backend .",
|
||||||
"rawbuild": "yarn tsc",
|
"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",
|
"author": "Aleksander <alekswilc> Wilczyński",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
|
@ -24,6 +24,7 @@ import { StatsRoute } from "./routes/stats.js";
|
|||||||
import { LogRoute } from "./routes/log.js";
|
import { LogRoute } from "./routes/log.js";
|
||||||
import { ActivePlayersRoute } from "./routes/activePlayer.js";
|
import { ActivePlayersRoute } from "./routes/activePlayer.js";
|
||||||
import { AdminRoute } from "./routes/admin.js";
|
import { AdminRoute } from "./routes/admin.js";
|
||||||
|
import { ImagesRoute } from './routes/images.js';
|
||||||
|
|
||||||
export class ApiModule
|
export class ApiModule
|
||||||
{
|
{
|
||||||
@ -39,6 +40,8 @@ export class ApiModule
|
|||||||
router.use("/leaderboard/", LeaderboardRoute.load());
|
router.use("/leaderboard/", LeaderboardRoute.load());
|
||||||
router.use("/active/", ActivePlayersRoute.load());
|
router.use("/active/", ActivePlayersRoute.load());
|
||||||
router.use("/admin/", AdminRoute.load());
|
router.use("/admin/", AdminRoute.load());
|
||||||
|
router.use("/images/", ImagesRoute.load());
|
||||||
|
|
||||||
|
|
||||||
router.use("/stats/", StatsRoute.load());
|
router.use("/stats/", StatsRoute.load());
|
||||||
router.use("/log/", LogRoute.load());
|
router.use("/log/", LogRoute.load());
|
||||||
|
@ -137,10 +137,80 @@ export const trainsList = [
|
|||||||
"4E/EU07-*",
|
"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) =>
|
export const getVehicle = (name: string) =>
|
||||||
{
|
{
|
||||||
return trainsList.find(x => wcmatch(x.pattern)(name))?.train;
|
return trainsList.find(x => wcmatch(x.pattern)(name))?.train;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -16,34 +16,38 @@
|
|||||||
|
|
||||||
import { Dispatch, SetStateAction } from "react";
|
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 }: {
|
export const Paginator = ({ page, setPage, pages }: {
|
||||||
page: number,
|
page: number,
|
||||||
pages: number,
|
pages: number,
|
||||||
setPage: Dispatch<SetStateAction<number>>
|
setPage: Dispatch<SetStateAction<number>>
|
||||||
}) =>
|
}) => {
|
||||||
{
|
// todo: rewrite this shit XDDDDDDDD
|
||||||
let numbers = [ 1, page - 2, page - 1, page, page + 1, page + 2 ];
|
const numbers = getPaginationNums(page, pages);
|
||||||
|
|
||||||
page === 1 && (numbers = [ page, page + 1, page + 2, page + 3, page + 4 ]);
|
|
||||||
|
|
||||||
page === 2 && (numbers = [ page - 1, page, page + 1, page + 2, page + 3 ]);
|
|
||||||
|
|
||||||
page === 3 && (numbers = [ page - 2, page - 1, page, page + 1, page + 2 ]);
|
|
||||||
|
|
||||||
(page === pages) && (numbers = [ 1, page - 4, page - 3, page - 2, page - 1 ]);
|
|
||||||
|
|
||||||
(page === (pages - 1)) && (numbers = [ 1, page - 3, page - 2, page - 1, page ]);
|
|
||||||
|
|
||||||
(page === (pages - 2)) && (numbers = [ 1, page - 2, page - 1, page, page + 1 ]);
|
|
||||||
|
|
||||||
numbers = numbers.filter(x => (pages + 1) >= x && x > 0);
|
|
||||||
|
|
||||||
return <div
|
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">
|
<ul className="flex flex-wrap items-center">
|
||||||
<li>
|
<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"
|
<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"
|
<svg className="fill-current" width="8" height="16" viewBox="0 0 8 16" fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M7.17578 15.1156C7.00703 15.1156 6.83828 15.0593 6.72578 14.9187L0.369531 8.44995C0.116406 8.19683 0.116406 7.80308 0.369531 7.54995L6.72578 1.0812C6.97891 0.828076 7.37266 0.828076 7.62578 1.0812C7.87891 1.33433 7.87891 1.72808 7.62578 1.9812L1.71953 7.99995L7.65391 14.0187C7.90703 14.2718 7.90703 14.6656 7.65391 14.9187C7.48516 15.0312 7.34453 15.1156 7.17578 15.1156Z"
|
<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>
|
</svg>
|
||||||
</a></li>
|
</a></li>
|
||||||
|
|
||||||
{ numbers.map(num =>
|
{numbers.map(num => {
|
||||||
{
|
|
||||||
return <li>
|
return <li>
|
||||||
<a onClick={ () => setPage(num) }
|
<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>
|
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>;
|
</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>
|
<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"
|
<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"
|
<svg className="fill-current" width="8" height="16" viewBox="0 0 8 16" fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M0.819531 15.1156C0.650781 15.1156 0.510156 15.0593 0.369531 14.9468C0.116406 14.6937 0.116406 14.3 0.369531 14.0468L6.27578 7.99995L0.369531 1.9812C0.116406 1.72808 0.116406 1.33433 0.369531 1.0812C0.622656 0.828076 1.01641 0.828076 1.26953 1.0812L7.62578 7.54995C7.87891 7.80308 7.87891 8.19683 7.62578 8.44995L1.26953 14.9187C1.15703 15.0312 0.988281 15.1156 0.819531 15.1156Z"
|
<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 { useState } from "react";
|
||||||
import { TProfileData } from "../../../types/profile.ts";
|
import { TProfileData, TProfilePlayer } from "../../../types/profile.ts";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ArrowIcon, FlexArrowIcon } from "../../mini/icons/ArrowIcon.tsx";
|
|
||||||
import { formatTime } from "../../../util/time.ts";
|
import { formatTime } from "../../../util/time.ts";
|
||||||
import { useAuth } from "../../../hooks/useAuth.tsx";
|
import { useAuth } from "../../../hooks/useAuth.tsx";
|
||||||
import { ConfirmModal } from "../../mini/modal/ConfirmModal.tsx";
|
import { ConfirmModal } from "../../mini/modal/ConfirmModal.tsx";
|
||||||
@ -25,78 +24,108 @@ import { post } from "../../../util/fetcher.ts";
|
|||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { UserIcons } from "../../mini/icons/UserIcons.tsx";
|
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);
|
export const ProfileCard = ({ data, images }: { data: TProfileData, images: TImagesData }) => {
|
||||||
const [ showStations, setShowStations ] = useState(false);
|
const [sortTrainsBy, setSortTrainsBy] = useState(0);
|
||||||
const [ sortTrainsBy, setSortTrainsBy ] = useState<"time" | "score" | "distance">("distance");
|
const [sortTrainsBy2, setSortTrainsBy2] = useState(2);
|
||||||
const [ hideLeaderboardStatsModal, setHideLeaderboardStatsModal ] = useState(false);
|
const [sortStationsBy, setSortStationsBy] = useState(0);
|
||||||
const [ hideProfileModal, setHideProfileModal ] = useState(false);
|
const [hideLeaderboardStatsModal, setHideLeaderboardStatsModal] = useState(false);
|
||||||
|
const [hideProfileModal, setHideProfileModal] = useState(false);
|
||||||
|
|
||||||
const { isAdmin, token } = useAuth();
|
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 })
|
post(`/admin/profile/${data.player.id}/${data.player.flags.includes("leaderboard_hidden") ? "showLeaderboard" : "hideLeaderboard"}`, {}, { "X-Auth-Token": token })
|
||||||
.then((response) =>
|
.then((response) => {
|
||||||
{
|
if (response.code === 200) {
|
||||||
if (response.code === 200)
|
|
||||||
{
|
|
||||||
toast.success(t("admin.hideLeaderboard.alert"));
|
toast.success(t("admin.hideLeaderboard.alert"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const adminHidePlayerProfile = () =>
|
const adminHidePlayerProfile = () => {
|
||||||
{
|
post(`/admin/profile/${data.player.id}/hide`, {}, { "X-Auth-Token": token })
|
||||||
post(`/admin/profile/${ data.player.id }/hide`, {}, { "X-Auth-Token": token })
|
.then((response) => {
|
||||||
.then((response) =>
|
if (response.code === 200) {
|
||||||
{
|
|
||||||
if (response.code === 200)
|
|
||||||
{
|
|
||||||
toast.success(t("admin.hide.alert"));
|
toast.success(t("admin.hide.alert"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const adminForceUpdate = () =>
|
const adminForceUpdate = () => {
|
||||||
{
|
post(`/admin/profile/${data.player.id}/forceUpdate`, {}, { "X-Auth-Token": token })
|
||||||
post(`/admin/profile/${ data.player.id }/forceUpdate`, {}, { "X-Auth-Token": token })
|
.then((response) => {
|
||||||
.then((response) =>
|
if (response.code === 200) {
|
||||||
{
|
|
||||||
if (response.code === 200)
|
|
||||||
{
|
|
||||||
toast.success(t("admin.update.alert"));
|
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();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<ConfirmModal showModal={ hideLeaderboardStatsModal } setShowModal={ setHideLeaderboardStatsModal }
|
<ConfirmModal showModal={hideLeaderboardStatsModal} setShowModal={setHideLeaderboardStatsModal}
|
||||||
onConfirm={ adminToggleHideLeaderboardPlayerProfile }
|
onConfirm={adminToggleHideLeaderboardPlayerProfile}
|
||||||
title={ t("admin.hideLeaderboard.modal.title") }
|
title={t("admin.hideLeaderboard.modal.title")}
|
||||||
description={ t("admin.hideLeaderboard.modal.description") }/>
|
description={t("admin.hideLeaderboard.modal.description")} />
|
||||||
<ConfirmModal showModal={ hideProfileModal } setShowModal={ setHideProfileModal }
|
<ConfirmModal showModal={hideProfileModal} setShowModal={setHideProfileModal}
|
||||||
onConfirm={ adminHidePlayerProfile } title={ t("admin.hide.modal.title") }
|
onConfirm={adminHidePlayerProfile} title={t("admin.hide.modal.title")}
|
||||||
description={ t("admin.hide.modal.description") }/>
|
description={t("admin.hide.modal.description")} />
|
||||||
<div
|
<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="px-4 pt-6 text-center lg:pb-8 xl:pb-11.5">
|
||||||
<div
|
<div
|
||||||
className="mx-auto max-w-44 rounded-full">
|
className="mx-auto max-w-44 rounded-full">
|
||||||
<div className="relative rounded-full">
|
<div className="relative rounded-full">
|
||||||
<img className="rounded-full" src={ data.player.avatar } alt="profile"/>
|
<img className="rounded-full" src={data.player.avatar} alt="profile" />
|
||||||
{ data.active &&
|
{data.active &&
|
||||||
<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> }
|
<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>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<h3 className="text-2xl font-semibold text-black dark:text-white">
|
<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>
|
</h3>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -104,179 +133,144 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
|||||||
<div
|
<div
|
||||||
className="flex flex-col items-center justify-center gap-1 border-r border-stroke px-4 dark:border-strokedark xsm:flex-row">
|
className="flex flex-col items-center justify-center gap-1 border-r border-stroke px-4 dark:border-strokedark xsm:flex-row">
|
||||||
<span className="font-semibold text-black dark:text-white">
|
<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>
|
||||||
<span className="text-sm text-wrap">{ t("profile.stats.distance") }</span>
|
<span className="text-sm text-wrap">{t("profile.stats.distance")}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="flex flex-col items-center justify-center gap-1 border-r border-stroke px-4 dark:border-strokedark xsm:flex-row">
|
className="flex flex-col items-center justify-center gap-1 border-r border-stroke px-4 dark:border-strokedark xsm:flex-row">
|
||||||
<span className="font-semibold text-black dark:text-white">
|
<span className="font-semibold text-black dark:text-white">
|
||||||
{ formatTime(data.player.dispatcherTime) }
|
{formatTime(data.player.dispatcherTime)}
|
||||||
</span>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ data.active && data.active.type === "train" &&
|
{data.active && data.active.type === "train" &&
|
||||||
<div className="mx-auto text-center">
|
<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>
|
<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> }
|
</div>}
|
||||||
|
|
||||||
{ data.active && data.active.type === "station" &&
|
{data.active && data.active.type === "station" &&
|
||||||
<div className="mx-auto text-center">
|
<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>
|
<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>
|
</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 &&
|
return <StationStat stationName={stationName} time={station.time} image={images.stations[stationName]} />
|
||||||
<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) }>
|
</div>
|
||||||
<h1 className="text-xl text-black dark:text-white pb-5">{ t("profile.trains.header") }</h1>
|
<div className="flex flex-col pt-4">
|
||||||
<ArrowIcon rotated={ showTrains }/>
|
<Paginator setPage={setDispatcherPage} page={dispatcherPage} pages={dispatcherStats.length} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ showTrains &&
|
<div className="px-5 pt-6 pb-5 sm:px-7.5 rounded-md">
|
||||||
<div className="flex flex-col rounded-sm border border-stroke dark:border-strokedark">
|
<h1 className="text-xl text-black dark:text-white pb-5">{t("profile.trains.header")}</h1>
|
||||||
<div className="grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-3">
|
<div className="flex flex-col">
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<a className='cursor-pointer' onClick={() => setSortTrainsBy2((prev) => prev === 0 ? 1 : 0)}><p className='text-base'>
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<strong>{t('profile.trains.sortby.title')}</strong> {sortTrainsBy2 === 0 ? t('profile.trains.sortby.max') : t('profile.trains.sortby.min')}
|
||||||
{ t("profile.trains.train") }
|
</p></a>
|
||||||
</h5>
|
<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>
|
||||||
<div className="flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5 cursor-pointer"
|
<div className="grid grid-cols-1 gap-7.5 sm:grid-cols-3 xl:grid-cols-4 pt-4">
|
||||||
onClick={ () => setSortTrainsBy("distance") }>
|
{trainStats[trainPage - 1].sort(sortTrains).map(trainName => {
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
const train = data.player.trainStats[trainName];
|
||||||
{ t("profile.trains.distance") }
|
return <TrainStat trainName={trainName} time={train.time} distance={train.distance} score={train.score} image={images.trains[trainName]} />
|
||||||
</h5>
|
})}
|
||||||
<FlexArrowIcon rotated={ sortTrainsBy === "distance" || !sortTrainsBy }/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5 cursor-pointer"
|
<div className="flex flex-col pt-4">
|
||||||
onClick={ () => setSortTrainsBy("score") }>
|
<Paginator setPage={setTrainPage} page={trainPage} pages={trainStats.length} />
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
|
||||||
{ t("profile.trains.points") }
|
|
||||||
</h5>
|
|
||||||
<FlexArrowIcon rotated={ sortTrainsBy === "score" }/>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{ Object.keys(data.player.trainStats).sort((a, b) => data.player.trainStats[ b ][ sortTrainsBy ] - data.player.trainStats[ a ][ sortTrainsBy ]).map(trainName =>
|
{/* <div className="bg-white px-5 pt-6 pb-5 shadow-default dark:bg-boxdark sm:px-7.5">
|
||||||
{
|
|
||||||
const train = data.player.trainStats[ trainName ];
|
|
||||||
|
|
||||||
return <div
|
|
||||||
className={ `grid grid-cols-3 sm:grid-cols-3 border-t border-t-stroke dark:border-t-strokedark` }
|
|
||||||
key={ trainName }
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
|
|
||||||
<p className="text-black dark:text-white sm:block break-all">
|
|
||||||
{ trainName }
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
|
||||||
<p className="text-meta-6 sm:block break-all">{ Math.floor(train.distance / 1000) }km</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
|
||||||
<p className="text-meta-3">{ train.score }</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/*<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">*/ }
|
|
||||||
{/* <p className="text-meta-3">{ formatTime(train.time) }</p>*/ }
|
|
||||||
{/*</div>*/ }
|
|
||||||
</div>;
|
|
||||||
}) }
|
|
||||||
|
|
||||||
|
|
||||||
</div> }
|
|
||||||
|
|
||||||
</div> }
|
|
||||||
{ Object.keys(data.player.dispatcherStats || {}).length > 0 &&
|
|
||||||
<div className="bg-white px-5 pt-6 pb-5 shadow-default dark:bg-boxdark sm:px-7.5">
|
|
||||||
<div className="group relative cursor-pointer" onClick={ () => setShowStations(val => !val) }>
|
|
||||||
<h1 className="text-xl text-black dark:text-white pb-5">{ t("profile.stations.header") }</h1>
|
|
||||||
<ArrowIcon rotated={ showStations }/>
|
|
||||||
</div>
|
|
||||||
{ showStations &&
|
|
||||||
<div className="flex flex-col rounded-sm border border-stroke dark:border-strokedark">
|
<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="grid grid-cols-2 rounded-sm bg-gray-2 dark:bg-meta-4">
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{ t("profile.stations.station") }
|
{t("profile.stations.station")}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-2.5 text-center xl:p-5">
|
<div className="p-2.5 text-center xl:p-5">
|
||||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||||
{ t("profile.stations.time") }
|
{t("profile.stations.time")}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
</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];
|
||||||
const station = data.player.dispatcherStats[ stationName ];
|
|
||||||
return <div
|
return <div
|
||||||
className={ `grid grid-cols-2 border-t border-t-stroke dark:border-t-strokedark` }
|
className={`grid grid-cols-2 border-t border-t-stroke dark:border-t-strokedark`}
|
||||||
key={ stationName }
|
key={stationName}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center gap-3 p-2.5 lg:p-5">
|
<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">
|
<p className="text-black dark:text-white sm:block break-all">
|
||||||
{ stationName }
|
{stationName}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
<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> }
|
</div>
|
||||||
|
|
||||||
</div> }
|
</div> */}
|
||||||
|
{isAdmin && <>
|
||||||
|
<div className="shadow-default items-center justify-center p-2.5 flex flex-col xl:p-5 gap-2">
|
||||||
{ isAdmin && <>
|
<h1 className="text-xl text-black dark:text-white">{t("admin.header")}</h1>
|
||||||
<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 className="items-center justify-center p-2.5 flex xl:p-5 gap-2 flex-wrap sm:flex-nowrap">
|
<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") ?
|
{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" }
|
<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() }>
|
onClick={() => adminToggleHideLeaderboardPlayerProfile()}>
|
||||||
{ t("admin.hideLeaderboard.button2") }
|
{t("admin.hideLeaderboard.button2")}
|
||||||
</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" }
|
<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) }>
|
onClick={() => setHideLeaderboardStatsModal(true)}>
|
||||||
{ t("admin.hideLeaderboard.button") }
|
{t("admin.hideLeaderboard.button")}
|
||||||
</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"
|
<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) }>
|
onClick={() => setHideProfileModal(true)}>
|
||||||
{ t("admin.hide.button") }
|
{t("admin.hide.button")}
|
||||||
</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"
|
<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() }>
|
onClick={() => adminForceUpdate()}>
|
||||||
{ t("admin.update.button") }
|
{t("admin.update.button")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -63,14 +63,27 @@
|
|||||||
"trains": {
|
"trains": {
|
||||||
"header": "Statistiky vlaků",
|
"header": "Statistiky vlaků",
|
||||||
"train": "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",
|
"distance": "Vzdálenost",
|
||||||
"points": "Body",
|
"score": "Body"
|
||||||
"time": "Čas"
|
}
|
||||||
},
|
},
|
||||||
"stations": {
|
"stations": {
|
||||||
"header": "Statistiky stanic",
|
"header": "Statistiky stanic",
|
||||||
"station": "Stanice",
|
"station": "Stanice",
|
||||||
"time": "Čas"
|
"time": "Čas: {{time}}",
|
||||||
|
"sortby": {
|
||||||
|
"title": "Řadit podle: ",
|
||||||
|
"min": "Nejnižší",
|
||||||
|
"max": "Nejvyšší"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"notfound": {
|
"notfound": {
|
||||||
|
@ -63,14 +63,27 @@
|
|||||||
"trains": {
|
"trains": {
|
||||||
"header": "Train Statistics",
|
"header": "Train Statistics",
|
||||||
"train": "Train",
|
"train": "Train",
|
||||||
|
"distance": "Distance: {{distance}}km",
|
||||||
|
"score": "Points: {{score}}",
|
||||||
|
"time": "Time: {{time}}",
|
||||||
|
"sortby": {
|
||||||
|
"title": "Sort: ",
|
||||||
|
"min": "Lowest",
|
||||||
|
"max": "Highest",
|
||||||
|
"time": "Time",
|
||||||
"distance": "Distance",
|
"distance": "Distance",
|
||||||
"points": "Points",
|
"score": "Points"
|
||||||
"time": "Time"
|
}
|
||||||
},
|
},
|
||||||
"stations": {
|
"stations": {
|
||||||
"header": "Station Statistics",
|
"header": "Station Statistics",
|
||||||
"station": "Station",
|
"station": "Station",
|
||||||
"time": "Time"
|
"time": "Time: {{time}}",
|
||||||
|
"sortby": {
|
||||||
|
"title": "Sort: ",
|
||||||
|
"min": "Lowest",
|
||||||
|
"max": "Highest"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"notfound": {
|
"notfound": {
|
||||||
|
@ -63,14 +63,27 @@
|
|||||||
"trains": {
|
"trains": {
|
||||||
"header": "Statystyki pociągów",
|
"header": "Statystyki pociągów",
|
||||||
"train": "Pociąg",
|
"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",
|
"distance": "Dystans",
|
||||||
"points": "Punkty",
|
"score": "Punkty"
|
||||||
"time": "Czas"
|
}
|
||||||
},
|
},
|
||||||
"stations": {
|
"stations": {
|
||||||
"header": "Statystyki stacji",
|
"header": "Statystyki stacji",
|
||||||
"station": "Stacja",
|
"station": "Stacja",
|
||||||
"time": "Czas"
|
"time": "Czas: {{time}}",
|
||||||
|
"sortby": {
|
||||||
|
"title": "Sortowanie: ",
|
||||||
|
"min": "Od najniższej",
|
||||||
|
"max": "Od najwyższej"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"notfound": {
|
"notfound": {
|
||||||
|
@ -27,36 +27,36 @@ import useSWR from "swr";
|
|||||||
import { get } from "../../util/fetcher.ts";
|
import { get } from "../../util/fetcher.ts";
|
||||||
|
|
||||||
|
|
||||||
export const Profile = () =>
|
export const Profile = () => {
|
||||||
{
|
|
||||||
const { id } = useParams();
|
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();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* LOADING */ }
|
{/* LOADING */}
|
||||||
{ isLoading && <ContentLoader/> }
|
{(isLoading || images.isLoading) && <ContentLoader />}
|
||||||
{/* ERROR */ }
|
{/* ERROR */}
|
||||||
{ error && <LoadError/> }
|
{(error || images.error) && <LoadError />}
|
||||||
{/* BLACKLISTED */ }
|
{/* BLACKLISTED */}
|
||||||
{ data && data.code === 403 && <PageMeta title="simrail.pro | Profile hidden"
|
{data && data.code === 403 && <PageMeta title="simrail.pro | Profile hidden"
|
||||||
description="The player's profile could not be displayed due to active moderator actions."/> }
|
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") }
|
{data && data.code === 403 && <WarningAlert title={t("profile.errors.blacklist.title")}
|
||||||
description={ t("profile.errors.blacklist.description") }/> }
|
description={t("profile.errors.blacklist.description")} />}
|
||||||
{/* NOT FOUND */ }
|
{/* NOT FOUND */}
|
||||||
{ data && data.code === 404 && <PageMeta title="simrail.pro | Profile 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."/> }
|
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") }
|
{data && data.code === 404 && <WarningAlert title={t("profile.errors.notfound.title")}
|
||||||
description={ t("profile.errors.notfound.description") }/> }
|
description={t("profile.errors.notfound.description")} />}
|
||||||
|
|
||||||
{/* SUCCESS */ }
|
{/* 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` }
|
title={`simrail.pro | ${data.data.player.username}'s profile`}
|
||||||
description={ `${ data.data.player.trainDistance ? 0 : ((data.data.player.trainDistance / 1000).toFixed(2)) } driving experience |
|
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.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.
|
* 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