Delete /t page

This commit is contained in:
Kalle
2026-01-26 19:54:08 +02:00
parent d9decda8d7
commit d44ebcec55
15 changed files with 86 additions and 277 deletions

View File

@@ -1,11 +1,11 @@
---
# sendou.ink-y2wo
title: Remove and redirect /t page
status: todo
status: completed
type: task
created_at: 2026-01-11T08:58:56Z
updated_at: 2026-01-11T08:58:56Z
parent: sendou.ink-6eko
---
Deprecate and remove the /t page. Add redirect to new location. Location: app/features/team/routes/t.tsx:51
Deprecate and remove the /t page. Add redirect to new location. Location: app/features/team/routes/t.tsx:51

View File

@@ -26,7 +26,6 @@ const NAV_CATEGORIES = [
items: [
{ name: "analyzer", url: "analyzer" },
{ name: "comp-analyzer", url: "comp-analyzer" },
{ name: "builds", url: "builds" },
{ name: "object-damage-calculator", url: "object-damage-calculator" },
{ name: "plans", url: "plans" },
{ name: "tier-list-maker", url: "tier-list-maker" },
@@ -35,8 +34,8 @@ const NAV_CATEGORIES = [
{
name: "community",
items: [
{ name: "builds", url: "builds" },
{ name: "badges", url: "badges" },
{ name: "t", url: "t" },
{ name: "vods", url: "vods" },
{ name: "art", url: "art" },
{ name: "articles", url: "a" },

View File

@@ -106,7 +106,7 @@ export function WeaponDestinationMenu({
selectedWeapon: SelectedWeapon;
onBack: () => void;
onSelect: (key: React.Key) => void;
listBoxRef: React.RefObject<HTMLDivElement>;
listBoxRef: React.RefObject<HTMLDivElement | null>;
}) {
const { t } = useTranslation(["common"]);

View File

@@ -91,11 +91,6 @@ export const navItems = [
url: "art",
prefetch: false,
},
{
name: "t",
url: "t",
prefetch: false,
},
{
name: "tier-list-maker",
url: "tier-list-maker",

View File

@@ -5,7 +5,7 @@ import {
dbReset,
wrappedAction,
} from "~/utils/Test";
import { action as teamIndexPageAction } from "../actions/t.server";
import { action as teamIndexPageAction } from "../actions/t.new.server";
import type { createTeamSchema } from "../team-schemas";
import type { editTeamSchema } from "../team-schemas.server";
import { action as _editTeamProfileAction } from "./t.$customUrl.edit.server";

View File

@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { dbInsertUsers, dbReset, wrappedAction } from "~/utils/Test";
import { action as teamIndexPageAction } from "../actions/t.server";
import { action as teamIndexPageAction } from "../actions/t.new.server";
import type { createTeamSchema } from "../team-schemas";
const action = wrappedAction<typeof createTeamSchema>({

View File

@@ -0,0 +1,13 @@
import { requireUser } from "~/features/auth/core/user.server";
import * as TeamRepository from "../TeamRepository.server";
export const loader = async () => {
const user = requireUser();
const teams = await TeamRepository.findAllUndisbanded();
const teamMemberOfCount = teams.filter((team) =>
team.members.some((m) => m.id === user.id),
).length;
return { teamMemberOfCount };
};

View File

@@ -1,63 +0,0 @@
import * as R from "remeda";
import type { UserWithPlusTier } from "~/db/tables";
import { getUser } from "~/features/auth/core/user.server";
import * as TeamRepository from "../TeamRepository.server";
export const loader = async () => {
const user = getUser();
const unsortedTeams = await TeamRepository.findAllUndisbanded();
const teams = unsortedTeams.sort((teamA, teamB) => {
// show own team first always
if (user && teamA.members.some((m) => m.id === user.id)) {
return -1;
}
if (user && teamB.members.some((m) => m.id === user.id)) {
return 1;
}
// then full teams
if (teamA.members.length >= 4 && teamB.members.length < 4) {
return -1;
}
if (teamA.members.length < 4 && teamB.members.length >= 4) {
return 1;
}
const teamAPlusTierRating = membersToCommonPlusTierRating(teamA.members);
const teamBPlusTierRating = membersToCommonPlusTierRating(teamB.members);
// and as tiebreaker teams with a higher plus server tier member first (4 best considered)
if (teamAPlusTierRating > teamBPlusTierRating) {
return 1;
}
if (teamAPlusTierRating < teamBPlusTierRating) {
return -1;
}
return 0;
});
return {
teams,
teamMemberOfCount: user
? teams.filter((team) => team.members.some((m) => m.id === user.id))
.length
: 0,
};
};
const membersToCommonPlusTierRating = (
members: Pick<UserWithPlusTier, "plusTier">[],
) => {
return R.sum(
members
.map((m) => m.plusTier ?? 100)
.sort((a, b) => a - b)
.slice(0, 4),
);
};

View File

@@ -5,7 +5,7 @@ import {
dbReset,
wrappedAction,
} from "~/utils/Test";
import { action as teamIndexPageAction } from "../actions/t.server";
import { action as teamIndexPageAction } from "../actions/t.new.server";
import { action as _editTeamAction } from "../routes/t.$customUrl.edit";
import type { createTeamSchema } from "../team-schemas";
import type { editTeamSchema } from "../team-schemas.server";

View File

@@ -11,7 +11,7 @@ import {
} from "~/utils/Test";
import { loader as userProfileLoader } from "../../user-page/loaders/u.$identifier.index.server";
import { action as _teamPageAction } from "../actions/t.$customUrl.index.server";
import { action as teamIndexPageAction } from "../actions/t.server";
import { action as teamIndexPageAction } from "../actions/t.new.server";
import { action as _editTeamAction } from "../routes/t.$customUrl.edit";
import * as TeamRepository from "../TeamRepository.server";
import type { createTeamSchema } from "../team-schemas";

View File

@@ -0,0 +1,60 @@
import { useTranslation } from "react-i18next";
import type { MetaFunction } from "react-router";
import { useLoaderData } from "react-router";
import { Alert } from "~/components/Alert";
import { Main } from "~/components/Main";
import { SendouForm } from "~/form";
import { useHasRole } from "~/modules/permissions/hooks";
import { metaTags } from "~/utils/remix";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { NEW_TEAM_PAGE, navIconUrl } from "~/utils/urls";
import { action } from "../actions/t.new.server";
import { loader } from "../loaders/t.new.server";
import { TEAM } from "../team-constants";
import { createTeamSchema } from "../team-schemas";
export { loader, action };
export const meta: MetaFunction = (args) => {
return metaTags({
title: "New team",
location: args.location,
});
};
export const handle: SendouRouteHandle = {
i18n: ["team", "forms"],
breadcrumb: () => ({
imgPath: navIconUrl("t"),
href: NEW_TEAM_PAGE,
type: "IMAGE",
}),
};
export default function NewTeamPage() {
const { t } = useTranslation(["team"]);
const data = useLoaderData<typeof loader>();
const isSupporter = useHasRole("SUPPORTER");
const canAddNewTeam = () => {
if (isSupporter) {
return data.teamMemberOfCount < TEAM.MAX_TEAM_COUNT_PATRON;
}
return data.teamMemberOfCount < TEAM.MAX_TEAM_COUNT_NON_PATRON;
};
return (
<Main className="stack lg half-width">
{!canAddNewTeam() ? (
<Alert variation="WARNING">
You can't add another team (max 2 for non-supporters and 5 for
supporters).
</Alert>
) : (
<SendouForm schema={createTeamSchema} title={t("team:newTeam.header")}>
{({ FormField }) => <FormField name="name" />}
</SendouForm>
)}
</Main>
);
}

View File

@@ -1,201 +1,5 @@
import { Search } from "lucide-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import type { MetaFunction } from "react-router";
import { Link, useLoaderData, useSearchParams } from "react-router";
import { AddNewButton } from "~/components/AddNewButton";
import { Alert } from "~/components/Alert";
import { SendouDialog } from "~/components/elements/Dialog";
import { Input } from "~/components/Input";
import { Main } from "~/components/Main";
import { Pagination } from "~/components/Pagination";
import { useUser } from "~/features/auth/core/user";
import { SendouForm } from "~/form";
import { usePagination } from "~/hooks/usePagination";
import { useHasRole } from "~/modules/permissions/hooks";
import { metaTags } from "~/utils/remix";
import type { SendouRouteHandle } from "~/utils/remix.server";
import {
NEW_TEAM_PAGE,
navIconUrl,
TEAM_SEARCH_PAGE,
teamPage,
} from "~/utils/urls";
import { action } from "../actions/t.server";
import { loader } from "../loaders/t.server";
import { TEAM, TEAMS_PER_PAGE } from "../team-constants";
import { createTeamSchema } from "../team-schemas";
export { loader, action };
import { redirect } from "react-router";
import styles from "../team.module.css";
export const meta: MetaFunction = (args) => {
return metaTags({
title: "Team Search",
ogTitle: "Splatoon team search",
description:
"List of all teams on sendou.ink and their members. Search for teams by name or member name.",
location: args.location,
});
export const loader = () => {
return redirect("/?search=open&type=teams");
};
export const handle: SendouRouteHandle = {
i18n: ["team", "forms"],
breadcrumb: () => ({
imgPath: navIconUrl("t"),
href: TEAM_SEARCH_PAGE,
type: "IMAGE",
}),
};
export default function TeamSearchPage() {
const { t, i18n } = useTranslation(["team"]);
const [inputValue, setInputValue] = React.useState("");
const data = useLoaderData<typeof loader>();
const filteredTeams = () => {
if (!inputValue) return data.teams;
const lowerCaseInput = inputValue.toLowerCase();
const matchingTeams = data.teams.filter((team) => {
if (team.name.toLowerCase().includes(lowerCaseInput)) return true;
if (team.tag && team.tag.toLowerCase() === lowerCaseInput) return true;
if (
team.members.some((m) =>
m.username.toLowerCase().includes(lowerCaseInput),
)
) {
return true;
}
return false;
});
return matchingTeams.sort((a, b) => {
const aTagExactMatch = a.tag && a.tag.toLowerCase() === lowerCaseInput;
const bTagExactMatch = b.tag && b.tag.toLowerCase() === lowerCaseInput;
if (aTagExactMatch && !bTagExactMatch) return -1;
if (!aTagExactMatch && bTagExactMatch) return 1;
return 0;
});
};
const {
itemsToDisplay,
everythingVisible,
currentPage,
pagesCount,
nextPage,
previousPage,
setPage,
} = usePagination({
items: filteredTeams(),
pageSize: TEAMS_PER_PAGE,
});
return (
<Main className="stack lg">
<NewTeamDialog />
<div className="stack sm horizontal justify-between">
<Input
className={styles.searchInput}
icon={<Search className={styles.searchIcon} />}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder={t("team:teamSearch.placeholder")}
testId="team-search-input"
/>
<AddNewButton navIcon="t" to={NEW_TEAM_PAGE} />
</div>
<div className="mt-6 stack lg">
{itemsToDisplay.map((team, i) => (
<Link
key={team.customUrl}
to={teamPage(team.customUrl)}
className={styles.searchTeam}
>
{team.avatarUrl ? (
<img
src={team.avatarUrl}
alt=""
width={64}
height={64}
className="rounded-full"
loading="lazy"
/>
) : (
<div className={styles.searchTeamAvatarPlaceholder}>
{team.name[0]}
</div>
)}
<div>
<div className={styles.searchTeamName} data-testid={`team-${i}`}>
{team.name}
{team.tag ? (
<span className={styles.searchTeamTag}>{team.tag}</span>
) : null}
</div>
<div className={styles.searchTeamMembers}>
{team.members.length === 1
? team.members[0].username
: new Intl.ListFormat(i18n.language, {
style: "short",
}).format(team.members.map((member) => member.username))}
</div>
</div>
</Link>
))}
</div>
{!everythingVisible ? (
<Pagination
currentPage={currentPage}
pagesCount={pagesCount}
nextPage={nextPage}
previousPage={previousPage}
setPage={setPage}
/>
) : null}
</Main>
);
}
function NewTeamDialog() {
const { t } = useTranslation(["common", "team"]);
const [searchParams] = useSearchParams();
const user = useUser();
const isSupporter = useHasRole("SUPPORTER");
const data = useLoaderData<typeof loader>();
const isOpen = searchParams.get("new") === "true";
const canAddNewTeam = () => {
if (!user) return false;
if (isSupporter) {
return data.teamMemberOfCount < TEAM.MAX_TEAM_COUNT_PATRON;
}
return data.teamMemberOfCount < TEAM.MAX_TEAM_COUNT_NON_PATRON;
};
if (isOpen && !canAddNewTeam()) {
return (
<Alert variation="WARNING">
You can't add another team (max 2 for non-supporters and 5 for
supporters).
</Alert>
);
}
return (
<SendouDialog
heading={t("team:newTeam.header")}
isOpen={isOpen}
onCloseTo={TEAM_SEARCH_PAGE}
>
<SendouForm schema={createTeamSchema}>
{({ FormField }) => <FormField name="name" />}
</SendouForm>
</SendouDialog>
);
}

View File

@@ -147,6 +147,7 @@ export default [
route("/support", "features/info/routes/support.tsx"),
route("/t", "features/team/routes/t.tsx"),
route("/t/new", "features/team/routes/t.new.tsx"),
route("/t/:customUrl", "features/team/routes/t.$customUrl.tsx", [
index("features/team/routes/t.$customUrl.index.tsx"),
route("edit", "features/team/routes/t.$customUrl.edit.tsx"),

View File

@@ -90,7 +90,7 @@ export const CONTRIBUTIONS_PAGE = "/contributions";
export const BADGES_PAGE = "/badges";
export const BUILDS_PAGE = "/builds";
export const TEAM_SEARCH_PAGE = "/t";
export const NEW_TEAM_PAGE = "/t?new=true";
export const NEW_TEAM_PAGE = "/t/new";
export const CALENDAR_PAGE = "/calendar";
export const CALENDAR_NEW_PAGE = "/calendar/new";
export const TOURNAMENT_NEW_PAGE = "/calendar/new?tournament=true";