Skip to content

Commit

Permalink
Implement leaderboard time trial ranking page (wip)
Browse files Browse the repository at this point in the history
  • Loading branch information
tmalahie committed Jun 21, 2022
1 parent 9e8ae1a commit e831f63
Show file tree
Hide file tree
Showing 11 changed files with 317 additions and 15 deletions.
3 changes: 3 additions & 0 deletions classement.global.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<?php
$cc = isset($_GET['cc']) ? $_GET['cc'] : 150;
header("location: /leaderboard/tt/$cc");
exit;
include('language.php');
include('session.php');
include('initdb.php');
Expand Down
Binary file added v2/app/images/icons/details.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion v2/app/pages/challenges/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ const ChallengesList: NextPage = () => {
<Link href="/challenges?moderate">{t("Back_to_challenges_list")}</Link>
<br />
</>}
<Link href="/">{t("Back_to_the_mario_kart")}</Link>
<Link href="/">{t("common:Back_to_mario_kart_pc")}</Link>
</p>
</div>
</ClassicPage>
Expand Down
22 changes: 17 additions & 5 deletions v2/app/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,9 @@ const Home: NextPage = () => {
case LeaderboardTab.BATTLE:
return "/leaderboard/battle";
case LeaderboardTab.TT_150:
return "classement.global.php?cc=150";
return "/leaderboard/tt/150";
case LeaderboardTab.TT_200:
return "classement.global.php?cc=200";
return "/leaderboard/tt/200";
}
}, [leaderboardTab]);

Expand Down Expand Up @@ -443,11 +443,23 @@ const Home: NextPage = () => {
const battleLeaderboardFiltered = useMemo(() => battleLeaderboard?.data.slice(0, 10) ?? [], [battleLeaderboard]);

const { data: tt150Leaderboard, loading: tt150Loading } = useSmoothFetch("/api/time-trial/leaderboard", {
placeholder: leaderboardPlaceholder
placeholder: leaderboardPlaceholder,
requestOptions: postData({
cc: 150,
paging: {
limit: 10
}
}),
});
const tt150LeaderboardFiltered = useMemo(() => tt150Leaderboard?.data.slice(0, 10) ?? [], [tt150Leaderboard]);
const { data: tt200Leaderboard, loading: tt200Loading } = useSmoothFetch("/api/time-trial/leaderboard?cc=200", {
placeholder: leaderboardPlaceholder
const { data: tt200Leaderboard, loading: tt200Loading } = useSmoothFetch("/api/time-trial/leaderboard", {
placeholder: leaderboardPlaceholder,
requestOptions: postData({
cc: 200,
paging: {
limit: 10
}
}),
});
const tt200LeaderboardFiltered = useMemo(() => tt200Leaderboard?.data.slice(0, 10) ?? [], [tt200Leaderboard]);

Expand Down
249 changes: 249 additions & 0 deletions v2/app/pages/leaderboard/tt/[cc].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import { NextPage } from "next";
import ClassicPage, {
commonStyles,
} from "../../../components/ClassicPage/ClassicPage";
import styles from "../../../styles/Leaderboard.module.scss";
import useLanguage from "../../../hooks/useLanguage";
import { useTranslation } from "next-i18next";
import withServerSideProps from "../../../components/WithAppContext/withServerSideProps";
import useSmoothFetch, { Placeholder, postData } from "../../../hooks/useSmoothFetch";
import { formatRank } from "../../../helpers/records";
import { usePaging } from "../../../hooks/usePaging";
import Pager from "../../../components/Pager/Pager";
import { useRouter } from "next/router";
import { useMemo, useState } from "react";
import Skeleton from "../../../components/Skeleton/Skeleton";
import Link from "next/link";
import Ad from "../../../components/Ad/Ad";
import Autocomplete from "../../../components/Autocomplete/Autocomplete";
import useDebounce from "../../../hooks/useDebounce";
import useFormSubmit, { doSubmit } from "../../../hooks/useFormSubmit";
import useAuthUser from "../../../hooks/useAuthUser";

import detailsIcon from "../../../images/icons/details.png"

const localesNs = ["leaderboard", "common"];

const OnlineLeaderboard: NextPage = () => {
const language = useLanguage();
const user = useAuthUser();
const { t } = useTranslation(localesNs);
const router = useRouter();
const cc = +(router.query.cc || 150);

const { paging, currentPage, setCurrentPage, resPerPage } = usePaging();

const playerQuery = router.query.player?.toString();
const { recordsPayload, recordsLoading } = useLeaderboardData(cc, paging, playerQuery);

const [player, setPlayer] = useState<string>(playerQuery || (user?.id ? user.name : ""));
const { playerSearchOptions } = usePlayerSearchData(player);

const handleSearch = useFormSubmit();

return (
<ClassicPage
title={t("Online_mode_leaderboard")}
className={styles.Leaderboard}
page="game"
>
<h1>{t("Global_Time_trial_leaderboard")}</h1>
<p dangerouslySetInnerHTML={{
__html: t("Leaderboard_explain")
}} />
<Ad width={728} height={90} bannerId="4919860724" />
<div className={styles["ranking-modes-ctn"]}>
<div>
<span>{t("Class")}</span>
<div className={styles["ranking-modes"]}>
{(cc != 150) ? (
<>
<Link href="/leaderboard/tt/150">150cc</Link>
<span>200cc</span>
</>
) : (
<>
<span>150cc</span>
<Link href="/leaderboard/tt/200">200cc</Link>
</>
)}
</div>
</div>
</div>
<form method="post" name="player" action={`/leaderboard/tt/${cc}`} onSubmit={handleSearch}>
<blockquote>
<div>
<label htmlFor="joueur">
<strong>{t("See_player_")}</strong>
</label>{" "}
<Autocomplete
type="text"
name="player"
id="joueur"
value={player}
items={playerSearchOptions}
onChange={(value) => setPlayer(value)}
onSelect={(_, e) => doSubmit(router, e.target.form)}
/>{" "}
<input
type="submit"
value={t<string>("common:validate")}
className={commonStyles.action_button}
/>
</div>
</blockquote>
</form>
{recordsPayload.count ? (
<Skeleton loading={recordsLoading}>
<table>
<tbody>
<tr id={styles.titres}>
<td>Place</td>
<td>{t("common:Nick")}</td>
<td>Score</td>
<td className={styles["cell-xs"]}>{t("Details")}</td>
</tr>
{recordsPayload.data.map((record, i) => (
<tr className={i % 2 ? styles.clair : styles.fonce} key={record.id}>
<td
dangerouslySetInnerHTML={{
__html: formatRank(language, record.rank),
}}
/>
<td>
<a
href={"/profil.php?id=" + record.id}
className={styles.recorder}
>
{record.country?.code && (
<img
src={`/images/flags/${record.country.code}.png`}
alt={record.country.code}
onError={(e) => (e.currentTarget.style.display = "none")}
/>
)}{" "}
{record.name}
</a>
</td>
<td className={styles["cell-auto"]}>{record.score}</td>
<td className={styles["cell-auto"]} title={t("See_records")}><a href={`classement.php?user=${record.id}&amp;cc=${cc}&amp;pts`}><img src={detailsIcon.src} className={styles.details} alt="Preview" /></a></td>
</tr>
))}
<tr>
<td colSpan={4} id={styles.page}>
{playerQuery ? <CurrentPage urlPrefix={`/leaderboard/tt/${cc}`} page={Math.ceil(recordsPayload.data[0]?.rank / resPerPage)} onSetPage={setCurrentPage} /> : <Pager
page={currentPage}
paging={paging}
count={recordsPayload.count}
onSetPage={setCurrentPage}
/>}
</td>
</tr>
</tbody>
</table>
</Skeleton>
) : (
<p>
<strong>{t("No_result_for_this_search")}</strong>
</p>
)}
<p>
<a href={`/classement.php?cc=${cc}`}>
{t("Ranking_by_circuit")}
</a>
<br />
<Link href="/"><a className={styles.retour}>{t("common:Back_to_mario_kart_pc")}</a></Link>
</p>
</ClassicPage>
);
};

function useLeaderboardData(cc: number, paging: any, player?: string) {
const { data: recordsPayload, loading: recordsLoading } = useSmoothFetch(
"/api/time-trial/leaderboard",
{
placeholder: () => ({
data: Placeholder.array(player ? 1 : 20, (id) => ({
id,
name: Placeholder.text(10, 20),
score: Placeholder.number(1000, 20000),
rank: id,
country: null as { code: string } | null,
})),
count: 1,
}),
requestOptions: postData({
name: player,
cc,
filters: [{
key: "deleted",
operator: "=",
value: 0
}],
paging
}),
reloadDeps: [cc, paging, player],
}
);

return { recordsPayload, recordsLoading };
}

function usePlayerSearchData(search: string) {
const debouncedPlayer = useDebounce(search, 300);

const { data: playerSearchData } = useSmoothFetch(
"/api/user/find",
{
placeholder: () => ({
data: Placeholder.array(0, (id) => ({
id,
name: Placeholder.text(10, 20)
})),
}),
requestOptions: postData({
filters: [{
key: "name",
operator: "~",
value: debouncedPlayer
}, {
key: "deleted",
operator: "=",
value: 0
}],
paging: {
page: 1,
limit: 5
}
}),
reloadDeps: [debouncedPlayer],
disabled: !debouncedPlayer
}
)
const playerSearchOptions = useMemo(() => {
if (!debouncedPlayer) return [];
if (!playerSearchData) return [];
return playerSearchData?.data.map((player) => ({
id: player.id,
label: player.name
}
))}, [debouncedPlayer, playerSearchData]);

return { playerSearchOptions, playerSearchData };
}

interface CurrentPageProps {
urlPrefix: string;
page: number;
onSetPage: (page: number) => void;
}
function CurrentPage({ urlPrefix, page, onSetPage }: CurrentPageProps) {
return <>
Page :{"  "}
<Link href={`${urlPrefix}?page=${page}`}><a onClick={() => onSetPage?.(page)}>{page}</a></Link>
</>;
}

export const getServerSideProps = withServerSideProps({ localesNs });

export default OnlineLeaderboard;
1 change: 0 additions & 1 deletion v2/app/public/locales/en/challenges.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"Rate_challenges": "Rate challenges",
"No_challenge_for_this": "No challenge for this search",
"Back_to_challenges_list": "Back to challenges list",
"Back_to_the_mario_kart": "Back to the Mario Kart PC",
"A_challenge_you_accepted": "A challenge you accepted or rejected by mistake? A difficulty to change? You're in the right place!",
"Filter_": "Filter:",
"Difficulty": "Difficulty...",
Expand Down
8 changes: 7 additions & 1 deletion v2/app/public/locales/en/leaderboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,11 @@
"Battle_mode": "Battle mode",
"See_player_": "See player:",
"No_result_for_this_search": "No results found for this search",
"Back_to_online_mode": "Back to online mode homepage"
"Ranking_by_circuit": "Ranking circuit by circuit",
"Back_to_online_mode": "Back to online mode homepage",
"Global_Time_trial_leaderboard": "Time Trial - Global ranking",
"Leaderboard_explain": "This page shows a leaderboard of top players in time trial.<br />This leaderboard is based on a score calculation which depends on your rank on each circuit. See <a href=\"topic.php?topic=5318\">this topic</a> for further info.",
"Class": "Class",
"Details": "Details",
"See_records": "See records"
}
1 change: 0 additions & 1 deletion v2/app/public/locales/fr/challenges.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"Rate_challenges": "Noter les défis",
"No_challenge_for_this": "Aucun défi trouvé",
"Back_to_challenges_list": "Retour à la liste des défis",
"Back_to_the_mario_kart": "Retour à Mario Kart PC",
"A_challenge_you_accepted": "Un défi que vous avez accepté ou refusé par erreur ? Une difficulté à changer ? C'est ici que ça se passe !",
"Filter_": "Filtrer :",
"Difficulty": "Difficulté...",
Expand Down
8 changes: 7 additions & 1 deletion v2/app/public/locales/fr/leaderboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,11 @@
"Battle_mode": "Bataille de ballons",
"See_player_": "Voir joueur :",
"No_result_for_this_search": "Aucun résultat trouvé pour cette recherche",
"Back_to_online_mode": "Retour à l'accueil du mode en ligne"
"Ranking_by_circuit": "Classement circuit par circuit",
"Back_to_online_mode": "Retour à l'accueil du mode en ligne",
"Global_Time_trial_leaderboard": "Contre-la-montre - Classement global",
"Leaderboard_explain": "Cette page affiche le classement des meilleurs joueurs en contre la montre.<br />Ce classement se base sur un calcul de score dépendant de votre place sur chaque circuit. Voir <a href=\"topic.php?topic=5318\">ce topic</a> pour en savoir plus.",
"Class": "Cylindrée",
"Details": "Détails",
"See_records": "Voir les temps"
}
24 changes: 24 additions & 0 deletions v2/app/styles/Leaderboard.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,30 @@
a:hover {
color: #fc0;
}
.details {
width: 20px;
}
.details:hover {
opacity: 0.7;
}
.cell-auto {
width: auto;
}
.cell-xs {
width: 20px;
}
.ranking-modes-ctn {
text-align: center;
margin-bottom: 10px;
}
.ranking-modes-ctn > div {
display: inline-flex;
align-items: center;
}
.ranking-modes-ctn > div > span {
font-weight: bold;
margin-right: 6px;
}
.ranking-modes.stripped > * {
border-right: solid 1px #820;
}
Expand Down
Loading

0 comments on commit e831f63

Please sign in to comment.