forked from simrail/simrail.pro
feat(frontend, backend): Sort leaderboard by points, distance and time.
This commit is contained in:
parent
c8d5d4afc8
commit
7d564ebbfc
@ -29,6 +29,12 @@ const generateSearch = (regex: RegExp) => [
|
||||
},
|
||||
];
|
||||
|
||||
const sortyByMap: Record<string, any> = {
|
||||
time: { trainTime: -1 },
|
||||
points: { trainPoints: -1 },
|
||||
distance: { trainDistance: -1 },
|
||||
}
|
||||
|
||||
export class LeaderboardRoute
|
||||
{
|
||||
static load()
|
||||
@ -48,9 +54,10 @@ export class LeaderboardRoute
|
||||
],
|
||||
},
|
||||
});
|
||||
const sortBy = sortyByMap[req.query.s?.toString() ?? 'distance'] ?? sortyByMap.distance;
|
||||
|
||||
const records = await MProfile.aggregate(filter)
|
||||
.sort({ trainPoints: -1 })
|
||||
.sort(sortBy)
|
||||
.limit(10);
|
||||
|
||||
res.json(
|
||||
|
@ -31,4 +31,24 @@ export const ArrowIcon = ({ rotated }: { rotated?: boolean }) =>
|
||||
d="M4.41107 6.9107C4.73651 6.58527 5.26414 6.58527 5.58958 6.9107L10.0003 11.3214L14.4111 6.91071C14.7365 6.58527 15.2641 6.58527 15.5896 6.91071C15.915 7.23614 15.915 7.76378 15.5896 8.08922L10.5896 13.0892C10.2641 13.4147 9.73651 13.4147 9.41107 13.0892L4.41107 8.08922C4.08563 7.76378 4.08563 7.23614 4.41107 6.9107Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>;
|
||||
|
||||
|
||||
export const FlexArrowIcon = ({ rotated }: { rotated?: boolean }) =>
|
||||
<svg
|
||||
className={ `fill-current ${
|
||||
rotated && "rotate-180"
|
||||
}` }
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.41107 6.9107C4.73651 6.58527 5.26414 6.58527 5.58958 6.9107L10.0003 11.3214L14.4111 6.91071C14.7365 6.58527 15.2641 6.58527 15.5896 6.91071C15.915 7.23614 15.915 7.76378 15.5896 8.08922L10.5896 13.0892C10.2641 13.4147 9.73651 13.4147 9.41107 13.0892L4.41107 8.08922C4.08563 7.76378 4.08563 7.23614 4.41107 6.9107Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>;
|
@ -20,9 +20,16 @@ import { TLeaderboardRecord } from "../../../types/leaderboard.ts";
|
||||
import { ContentLoader } from "../../mini/loaders/ContentLoader.tsx";
|
||||
import { WarningAlert } from "../../mini/alerts/Warning.tsx";
|
||||
import { formatTime } from "../../../util/time.ts";
|
||||
import { FaCheck } from 'react-icons/fa6';
|
||||
import { FaCheck } from "react-icons/fa6";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { FlexArrowIcon } from "../../mini/icons/ArrowIcon.tsx";
|
||||
|
||||
export const TrainTable = ({ trains, error }: { trains: TLeaderboardRecord[], error: number }) =>
|
||||
export const TrainTable = ({ trains, error, setSortBy, sortBy }: {
|
||||
trains: TLeaderboardRecord[],
|
||||
error: number,
|
||||
setSortBy: Dispatch<SetStateAction<string>>
|
||||
sortBy: string
|
||||
}) =>
|
||||
{
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -35,26 +42,32 @@ export const TrainTable = ({ trains, error }: { trains: TLeaderboardRecord[], er
|
||||
{ error === 1 && <div
|
||||
className="rounded-sm border border-stroke bg-white px-5 pt-6 pb-2.5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
|
||||
<div className="flex flex-col">
|
||||
<div className="grid grid-cols-4 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-5">
|
||||
<div className="grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-5">
|
||||
<div className="p-2.5 text-center xl:p-5">
|
||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||
{ t("leaderboard.user") }
|
||||
</h5>
|
||||
</div>
|
||||
<div className="p-2.5 text-center xl:p-5">
|
||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||
{ t("leaderboard.points") }
|
||||
</h5>
|
||||
</div>
|
||||
<div className="p-2.5 text-center xl:p-5">
|
||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||
<div className="flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5">
|
||||
<h5 className="cursor-pointer text-sm font-medium uppercase xsm:text-base"
|
||||
onClick={ () => setSortBy("distance") }>
|
||||
{ t("leaderboard.distance") }
|
||||
</h5>
|
||||
<FlexArrowIcon rotated={ !(sortBy === "distance") }/>
|
||||
</div>
|
||||
<div className="p-2.5 text-center sm:block xl:p-5">
|
||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||
<div className="flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5">
|
||||
<h5 className="cursor-pointer text-sm font-medium uppercase xsm:text-base"
|
||||
onClick={ () => setSortBy("points") }>
|
||||
{ t("leaderboard.points") }
|
||||
</h5>
|
||||
<FlexArrowIcon rotated={ !(sortBy === "points") }/>
|
||||
</div>
|
||||
<div className="hidden sm:flex flex-row align-center justify-center gap-2 p-2.5 text-center xl:p-5">
|
||||
<h5 className="cursor-pointer text-sm font-medium uppercase xsm:text-base"
|
||||
onClick={ () => setSortBy("time") }>
|
||||
{ t("leaderboard.time") }
|
||||
</h5>
|
||||
<FlexArrowIcon rotated={ !(sortBy === "time") }/>
|
||||
</div>
|
||||
<div className="hidden p-2.5 text-center sm:block xl:p-5">
|
||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||
@ -65,7 +78,7 @@ export const TrainTable = ({ trains, error }: { trains: TLeaderboardRecord[], er
|
||||
|
||||
{ trains.map((train, key) => (
|
||||
<div
|
||||
className={ `grid grid-cols-4 sm:grid-cols-5 ${ trains.length === (key + 1)
|
||||
className={ `grid grid-cols-3 sm:grid-cols-5 ${ trains.length === (key + 1)
|
||||
? ""
|
||||
: "border-b border-stroke dark:border-strokedark"
|
||||
}` }
|
||||
@ -74,19 +87,20 @@ export const TrainTable = ({ trains, error }: { trains: TLeaderboardRecord[], er
|
||||
<div className="flex items-center justify-center gap-3 p-5 lg:p-5">
|
||||
<p className="text-black dark:text-white sm:block break-all">
|
||||
<Link to={ "/profile/" + train.steam }
|
||||
className="color-orchid">{ train.steamName }</Link> { train.verified && <FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
||||
className="color-orchid">{ train.steamName }</Link> { train.verified &&
|
||||
<FaCheck className={ "inline text-meta-3 ml-1" }/> }
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||
<p className="text-meta-6">{ train.trainPoints }</p>
|
||||
<p className="text-meta-6">{ (train.trainDistance / 1000).toFixed(2) }km</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||
<p className="text-meta-5">{ (train.trainDistance / 1000).toFixed(2) }km</p>
|
||||
<p className="text-meta-5">{ train.trainPoints }</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||
<div className="hidden sm:flex items-center justify-center p-2.5 lg:p-5">
|
||||
<p className="text-meta-3">{ formatTime(train.trainTime) }</p>
|
||||
</div>
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
import { useState } from "react";
|
||||
import { TProfileData } from "../../../types/profile.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ArrowIcon } from "../../mini/icons/ArrowIcon.tsx";
|
||||
import { ArrowIcon, FlexArrowIcon } from "../../mini/icons/ArrowIcon.tsx";
|
||||
import { formatTime } from "../../../util/time.ts";
|
||||
import { FaCheck } from "react-icons/fa6";
|
||||
|
||||
@ -26,7 +26,7 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
||||
|
||||
const [ showTrains, setShowTrains ] = useState(false);
|
||||
const [ showStations, setShowStations ] = useState(false);
|
||||
const [ sortTrainsBy, setSortTrainsBy ] = useState<"time" | "score" | "distance">("score");
|
||||
const [ sortTrainsBy, setSortTrainsBy ] = useState<"time" | "score" | "distance">("distance");
|
||||
|
||||
const { t } = useTranslation();
|
||||
return <div
|
||||
@ -78,23 +78,26 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
||||
{ t("profile.trains.train") }
|
||||
</h5>
|
||||
</div>
|
||||
<div className="p-2.5 text-center xl:p-5 cursor-pointer"
|
||||
<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") }/>
|
||||
</div>
|
||||
<div className="hidden sm:block p-2.5 text-center xl:p-5 cursor-pointer"
|
||||
<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="p-2.5 text-center xl:p-5 cursor-pointer"
|
||||
<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>
|
||||
|
||||
@ -116,11 +119,11 @@ export const ProfileCard = ({ data }: { data: TProfileData }) =>
|
||||
<p className="text-meta-6 sm:block break-all">{ Math.floor(train.distance / 1000) }km</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden sm: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">{ train.score }</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 lg:p-5">
|
||||
<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>;
|
||||
|
@ -26,6 +26,8 @@ export const TrainLeaderboard = () =>
|
||||
const [ data, setData ] = useState<TLeaderboardRecord[]>([]);
|
||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||
const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? "");
|
||||
const [ sortBy, setSortBy ] = useState(searchParams.get("distance") ?? "");
|
||||
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
@ -43,14 +45,19 @@ export const TrainLeaderboard = () =>
|
||||
searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue);
|
||||
setSearchParams(searchParams);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
searchValue && params.set('q', searchValue);
|
||||
sortBy && params.set('s', sortBy);
|
||||
|
||||
setData([]);
|
||||
setError(0);
|
||||
fetch(`${ import.meta.env.VITE_API_URL }/leaderboard/train/?q=${ searchValue }`).then(x => x.json()).then(x =>
|
||||
fetch(`${ import.meta.env.VITE_API_URL }/leaderboard/train/?${params.toString()}`).then(x => x.json()).then(x =>
|
||||
{
|
||||
setData(x.data.records);
|
||||
setError(x.data.records.length > 0 ? 1 : 2);
|
||||
});
|
||||
}, [ searchValue ]);
|
||||
}, [ searchValue, sortBy ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
@ -66,7 +73,7 @@ export const TrainLeaderboard = () =>
|
||||
<>
|
||||
<div className="flex flex-col gap-10">
|
||||
<Search handleInputChange={ handleInputChange } searchItem={ searchItem }/>
|
||||
<TrainTable trains={ data } error={ error }/>
|
||||
<TrainTable trains={ data } error={ error } setSortBy={setSortBy} sortBy={sortBy}/>
|
||||
|
||||
</div>
|
||||
</>
|
||||
|
Loading…
x
Reference in New Issue
Block a user