feat(frontend, backend): Sort leaderboard by points, distance and time.

This commit is contained in:
Aleksander Wilczyński 2024-11-19 22:17:09 +01:00
parent c8d5d4afc8
commit 7d564ebbfc
Signed by untrusted user: alekswilc
GPG Key ID: D4464A248E5F27FE
5 changed files with 79 additions and 28 deletions

View File

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

View File

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

View File

@ -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>

View File

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

View File

@ -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>
</>