mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-07-26 13:28:32 -05:00
User results page search
This commit is contained in:
parent
c0395cc1bc
commit
a4eda293e7
|
|
@ -544,13 +544,27 @@ const baseTournamentResultsQuery = (userId: number) =>
|
|||
.innerJoin("Tournament", "Tournament.id", "TournamentResult.tournamentId")
|
||||
.where("TournamentResult.userId", "=", userId);
|
||||
|
||||
const escapeLikePattern = (value: string) =>
|
||||
value.replace(/[\\%_]/g, (char) => `\\${char}`);
|
||||
|
||||
const tournamentNameLikeExpr = (tournamentName: string) => {
|
||||
const pattern = `%${escapeLikePattern(tournamentName)}%`;
|
||||
return sql<boolean>`${sql.ref("CalendarEvent.name")} like ${pattern} escape '\\'`;
|
||||
};
|
||||
|
||||
export function findResultsByUserId(
|
||||
userId: number,
|
||||
{
|
||||
showHighlightsOnly = false,
|
||||
limit,
|
||||
offset,
|
||||
}: { showHighlightsOnly?: boolean; limit?: number; offset?: number } = {},
|
||||
tournamentName,
|
||||
}: {
|
||||
showHighlightsOnly?: boolean;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
tournamentName?: string;
|
||||
} = {},
|
||||
) {
|
||||
let calendarEventResultsQuery = baseCalendarEventResultsQuery(userId).select(
|
||||
({ eb, fn }) => [
|
||||
|
|
@ -634,6 +648,15 @@ export function findResultsByUserId(
|
|||
);
|
||||
}
|
||||
|
||||
if (tournamentName) {
|
||||
calendarEventResultsQuery = calendarEventResultsQuery.where(
|
||||
tournamentNameLikeExpr(tournamentName),
|
||||
);
|
||||
tournamentResultsQuery = tournamentResultsQuery.where(
|
||||
tournamentNameLikeExpr(tournamentName),
|
||||
);
|
||||
}
|
||||
|
||||
let query = calendarEventResultsQuery
|
||||
.unionAll(tournamentResultsQuery)
|
||||
.orderBy("startTime", "desc")
|
||||
|
|
@ -652,7 +675,10 @@ export function findResultsByUserId(
|
|||
|
||||
export async function countResultsByUserId(
|
||||
userId: number,
|
||||
{ showHighlightsOnly = false }: { showHighlightsOnly?: boolean } = {},
|
||||
{
|
||||
showHighlightsOnly = false,
|
||||
tournamentName,
|
||||
}: { showHighlightsOnly?: boolean; tournamentName?: string } = {},
|
||||
) {
|
||||
let calendarEventResultsQuery = baseCalendarEventResultsQuery(userId).select(
|
||||
({ fn }) => [fn.countAll<number>().as("count")],
|
||||
|
|
@ -675,6 +701,15 @@ export async function countResultsByUserId(
|
|||
);
|
||||
}
|
||||
|
||||
if (tournamentName) {
|
||||
calendarEventResultsQuery = calendarEventResultsQuery.where(
|
||||
tournamentNameLikeExpr(tournamentName),
|
||||
);
|
||||
tournamentResultsQuery = tournamentResultsQuery.where(
|
||||
tournamentNameLikeExpr(tournamentName),
|
||||
);
|
||||
}
|
||||
|
||||
const [calendarEventResults, tournamentResults] = await Promise.all([
|
||||
calendarEventResultsQuery.executeTakeFirst(),
|
||||
tournamentResultsQuery.executeTakeFirst(),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import type { LoaderFunctionArgs } from "react-router";
|
||||
import { redirect } from "react-router";
|
||||
import { getUser } from "~/features/auth/core/user.server";
|
||||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
import type { SerializeFrom } from "~/utils/remix";
|
||||
import { notFoundIfFalsy, parseSafeSearchParams } from "~/utils/remix.server";
|
||||
import { RESULTS_PER_PAGE } from "../user-page-constants";
|
||||
import {
|
||||
HIGHLIGHTS_RESULTS_MAX,
|
||||
RESULTS_PER_PAGE,
|
||||
} from "../user-page-constants";
|
||||
import { userResultsPageSearchParamsSchema } from "../user-page-schemas";
|
||||
|
||||
export type UserResultsLoaderData = SerializeFrom<typeof loader>;
|
||||
|
|
@ -34,15 +38,23 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
|||
}
|
||||
|
||||
const page = parsedSearchParams.success ? parsedSearchParams.data.page : 1;
|
||||
const tournamentName =
|
||||
!isChoosingHighlights && getUser() && parsedSearchParams.success
|
||||
? parsedSearchParams.data.tournament
|
||||
: undefined;
|
||||
|
||||
const [results, totalCount] = await Promise.all([
|
||||
UserRepository.findResultsByUserId(userId, {
|
||||
showHighlightsOnly,
|
||||
tournamentName,
|
||||
...(isChoosingHighlights
|
||||
? {}
|
||||
? { limit: HIGHLIGHTS_RESULTS_MAX }
|
||||
: { limit: RESULTS_PER_PAGE, offset: (page - 1) * RESULTS_PER_PAGE }),
|
||||
}),
|
||||
UserRepository.countResultsByUserId(userId, { showHighlightsOnly }),
|
||||
UserRepository.countResultsByUserId(userId, {
|
||||
showHighlightsOnly,
|
||||
tournamentName,
|
||||
}),
|
||||
]);
|
||||
|
||||
const maxPage = Math.ceil(totalCount / RESULTS_PER_PAGE);
|
||||
|
|
@ -72,6 +84,6 @@ function redirectIfPageOutOfBounds({
|
|||
|
||||
const url = new URL(request.url);
|
||||
const searchParams = new URLSearchParams(url.searchParams);
|
||||
searchParams.set("page", String(maxPage));
|
||||
searchParams.set("page", String(Math.max(maxPage, 1)));
|
||||
throw redirect(`${url.pathname}?${searchParams.toString()}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { Search } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLoaderData, useMatches, useSearchParams } from "react-router";
|
||||
import { useDebounce } from "react-use";
|
||||
import { LinkButton } from "~/components/elements/Button";
|
||||
import { Input } from "~/components/Input";
|
||||
import { Pagination } from "~/components/Pagination";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { UserResultsTable } from "~/features/user-page/components/UserResultsTable";
|
||||
|
|
@ -10,6 +14,7 @@ import { SendouButton } from "../../../components/elements/Button";
|
|||
import { SubPageHeader } from "../components/SubPageHeader";
|
||||
import { loader } from "../loaders/u.$identifier.results.server";
|
||||
import type { UserPageLoaderData } from "../loaders/u.$identifier.server";
|
||||
import styles from "../user-page.module.css";
|
||||
|
||||
export { loader };
|
||||
|
||||
|
|
@ -25,6 +30,34 @@ export default function UserResultsPage() {
|
|||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const showAll = searchParams.get("all") === "true";
|
||||
|
||||
const urlTournamentQuery = searchParams.get("tournament") ?? "";
|
||||
const [tournamentQuery, setTournamentQuery] =
|
||||
React.useState(urlTournamentQuery);
|
||||
const [prevUrlTournamentQuery, setPrevUrlTournamentQuery] =
|
||||
React.useState(urlTournamentQuery);
|
||||
|
||||
if (urlTournamentQuery !== prevUrlTournamentQuery) {
|
||||
setPrevUrlTournamentQuery(urlTournamentQuery);
|
||||
setTournamentQuery(urlTournamentQuery);
|
||||
}
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
if (urlTournamentQuery === tournamentQuery) return;
|
||||
setSearchParams((params) => {
|
||||
if (tournamentQuery) {
|
||||
params.set("tournament", tournamentQuery);
|
||||
} else {
|
||||
params.delete("tournament");
|
||||
}
|
||||
params.delete("page");
|
||||
return params;
|
||||
});
|
||||
},
|
||||
300,
|
||||
[tournamentQuery],
|
||||
);
|
||||
|
||||
const setPage = (page: number) => {
|
||||
setSearchParams((params) => {
|
||||
params.set("page", String(page));
|
||||
|
|
@ -44,15 +77,23 @@ export default function UserResultsPage() {
|
|||
? t("results.title")
|
||||
: t("results.highlights")}
|
||||
</h2>
|
||||
{user?.id === layoutData.user.id ? (
|
||||
<LinkButton
|
||||
to={userResultsEditHighlightsPage(user)}
|
||||
className="ml-auto"
|
||||
size="small"
|
||||
>
|
||||
{t("results.highlights.choose")}
|
||||
</LinkButton>
|
||||
) : null}
|
||||
<div className="stack horizontal sm items-center">
|
||||
{user ? (
|
||||
<Input
|
||||
className={styles.resultsFilterInput}
|
||||
value={tournamentQuery}
|
||||
onChange={(e) => setTournamentQuery(e.target.value)}
|
||||
placeholder={t("results.filter.placeholder")}
|
||||
aria-label={t("results.filter.placeholder")}
|
||||
icon={<Search />}
|
||||
/>
|
||||
) : null}
|
||||
{user?.id === layoutData.user.id ? (
|
||||
<LinkButton to={userResultsEditHighlightsPage(user)} size="small">
|
||||
{t("results.highlights.choose")}
|
||||
</LinkButton>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<UserResultsTable id="user-results-table" results={data.results.value} />
|
||||
{data.results.pages > 1 ? (
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export const IN_GAME_NAME_REGEXP = /^.{1,10}#[0-9a-z]{4,5}$/u;
|
|||
|
||||
export const MATCHES_PER_SEASONS_PAGE = 8;
|
||||
export const RESULTS_PER_PAGE = 25;
|
||||
export const HIGHLIGHTS_RESULTS_MAX = 500;
|
||||
export const DEFAULT_BUILD_SORT = ["WEAPON_POOL", "UPDATED_AT"] as const;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -199,6 +199,7 @@ export const adminTabActionSchema = z.union([
|
|||
export const userResultsPageSearchParamsSchema = z.object({
|
||||
all: z.stringbool().catch(false),
|
||||
page: z.coerce.number().min(1).max(1_000).catch(1),
|
||||
tournament: z.string().trim().min(1).max(100).optional().catch(undefined),
|
||||
});
|
||||
|
||||
const widgetSettingsSchemas = allWidgetsFlat().map((widget) => {
|
||||
|
|
|
|||
|
|
@ -126,6 +126,11 @@
|
|||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.resultsFilterInput {
|
||||
width: 12rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.resultsTableHighlights {
|
||||
border: var(--s-2) solid var(--color-bg-high);
|
||||
padding-inline: 0 !important;
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@
|
|||
"results.highlights.explanation": "Vælg de resultater, som du vil fremhæve",
|
||||
"results.button.showHighlights": "Vis højdepunkter",
|
||||
"results.button.showAll": "Vis alt",
|
||||
"results.filter.placeholder": "",
|
||||
"forms.errors.maxWeapons": "Maks antal våben nået",
|
||||
"search.info": "",
|
||||
"search.noResults": "Søgningen ’{{query}}’ fandt ingen brugere",
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@
|
|||
"results.highlights.explanation": "Wähle Ergebnisse, die du hervorheben möchtest",
|
||||
"results.button.showHighlights": "",
|
||||
"results.button.showAll": "",
|
||||
"results.filter.placeholder": "",
|
||||
"forms.errors.maxWeapons": "Maximale Zahl an Waffen erreicht",
|
||||
"search.info": "",
|
||||
"search.noResults": "Keine Nutzer gefunden, die '{{query}}' entsprechen",
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@
|
|||
"results.highlights.explanation": "Select the results you want to highlight",
|
||||
"results.button.showHighlights": "Show highlights",
|
||||
"results.button.showAll": "Show all",
|
||||
"results.filter.placeholder": "Filter by tournament",
|
||||
"forms.errors.maxWeapons": "Max weapon count reached",
|
||||
"search.info": "Search for users by Discord or Splatoon 3 name",
|
||||
"search.noResults": "No users found matching '{{query}}'",
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@
|
|||
"results.highlights.explanation": "Elige los resultados que quieres resaltar",
|
||||
"results.button.showHighlights": "Mostrar resaltados",
|
||||
"results.button.showAll": "Mostrar todos",
|
||||
"results.filter.placeholder": "",
|
||||
"forms.errors.maxWeapons": "Máxima cantidad de armas",
|
||||
"search.info": "Busca usuarios por su nombre de Discord o de Splatoon 3",
|
||||
"search.noResults": "No se encontraron usuarios que coincidan con '{{query}}'",
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@
|
|||
"results.highlights.explanation": "Elige los resultados que quieres resaltar",
|
||||
"results.button.showHighlights": "Mostrar resaltos",
|
||||
"results.button.showAll": "Mostrar todos",
|
||||
"results.filter.placeholder": "",
|
||||
"forms.errors.maxWeapons": "Máxima cantidad de armas",
|
||||
"search.info": "",
|
||||
"search.noResults": "No se encontraron usuarios que coincidan con '{{query}}'",
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@
|
|||
"results.highlights.explanation": "Sélectionnez les résultats que vous voulez mettre en avant",
|
||||
"results.button.showHighlights": "",
|
||||
"results.button.showAll": "",
|
||||
"results.filter.placeholder": "",
|
||||
"forms.errors.maxWeapons": "Nombre d'armes maximum atteint",
|
||||
"search.info": "",
|
||||
"search.noResults": "Aucun utilisateur correspondant à '{{query}}' n'a été trouvé",
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@
|
|||
"results.highlights.explanation": "Sélectionnez les résultats que vous voulez mettre en avant",
|
||||
"results.button.showHighlights": "Montrer les highlights",
|
||||
"results.button.showAll": "Tout montrer",
|
||||
"results.filter.placeholder": "",
|
||||
"forms.errors.maxWeapons": "Nombre d'armes maximum atteint",
|
||||
"search.info": "Recherchez avec le pseudo Discord ou Splatoon 3 du compte",
|
||||
"search.noResults": "Aucun utilisateur correspondant à '{{query}}' n'a été trouvé",
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@
|
|||
"results.highlights.explanation": "בחרו את התוצאות שאתם רוצים להדגיש",
|
||||
"results.button.showHighlights": "",
|
||||
"results.button.showAll": "",
|
||||
"results.filter.placeholder": "",
|
||||
"forms.errors.maxWeapons": "הגעה לכמות מקסימלית של מספר נשקים",
|
||||
"search.info": "",
|
||||
"search.noResults": "לא נמצאו משתמשים התואמים '{{query}}'",
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@
|
|||
"results.highlights.explanation": "Scegli il risultato che vuoi mettere come highlight",
|
||||
"results.button.showHighlights": "Mostra highlight",
|
||||
"results.button.showAll": "Mostra tutti",
|
||||
"results.filter.placeholder": "",
|
||||
"forms.errors.maxWeapons": "Massimo numero di armi raggiunto",
|
||||
"search.info": "Cerca utenti tramite nome Discord o Splatoon 3",
|
||||
"search.noResults": "Nessun utente trovato per '{{query}}'",
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@
|
|||
"results.highlights.explanation": "戦績として選択したい結果を選ぶ",
|
||||
"results.button.showHighlights": "ハイライトを表示",
|
||||
"results.button.showAll": "全て表示",
|
||||
"results.filter.placeholder": "",
|
||||
"forms.errors.maxWeapons": "最大ブキ数を超えました",
|
||||
"search.info": "",
|
||||
"search.noResults": "該当ユーザーが見つかりません '{{query}}'",
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@
|
|||
"results.highlights.explanation": "",
|
||||
"results.button.showHighlights": "",
|
||||
"results.button.showAll": "",
|
||||
"results.filter.placeholder": "",
|
||||
"forms.errors.maxWeapons": "",
|
||||
"search.info": "",
|
||||
"search.noResults": "",
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@
|
|||
"results.highlights.explanation": "",
|
||||
"results.button.showHighlights": "",
|
||||
"results.button.showAll": "",
|
||||
"results.filter.placeholder": "",
|
||||
"forms.errors.maxWeapons": "",
|
||||
"search.info": "",
|
||||
"search.noResults": "",
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@
|
|||
"results.highlights.explanation": "Wybierz wyniki, które chcesz wyróżnić",
|
||||
"results.button.showHighlights": "",
|
||||
"results.button.showAll": "",
|
||||
"results.filter.placeholder": "",
|
||||
"forms.errors.maxWeapons": "Maksymalna ilość broni osiągnięta",
|
||||
"search.info": "",
|
||||
"search.noResults": "Nie znaleziono użytkownika o nazwie '{{query}}'",
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@
|
|||
"results.highlights.explanation": "Escolha os resultados que você quer destacar",
|
||||
"results.button.showHighlights": "",
|
||||
"results.button.showAll": "",
|
||||
"results.filter.placeholder": "",
|
||||
"forms.errors.maxWeapons": "Número máximo de armas no perfil atingido.",
|
||||
"search.info": "",
|
||||
"search.noResults": "Nenhum usuário encontrado com o termo '{{query}}'",
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@
|
|||
"results.highlights.explanation": "Выберите ваш избранный результат",
|
||||
"results.button.showHighlights": "Показать избранные",
|
||||
"results.button.showAll": "Показать все",
|
||||
"results.filter.placeholder": "",
|
||||
"forms.errors.maxWeapons": "Достигнут максимум",
|
||||
"search.info": "Поиск пользователей по имени Discord или Splatoon 3",
|
||||
"search.noResults": "По запросу '{{query}}' пользователь не найден",
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@
|
|||
"results.highlights.explanation": "选择您想强调的高光成绩",
|
||||
"results.button.showHighlights": "显示高光成绩",
|
||||
"results.button.showAll": "显示全部成绩",
|
||||
"results.filter.placeholder": "",
|
||||
"forms.errors.maxWeapons": "已达到武器数量上限",
|
||||
"search.info": "",
|
||||
"search.noResults": "没有符合 '{{query}}' 的用户",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user