mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-28 14:18:04 -05:00
Delete /t page
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>({
|
||||
13
app/features/team/loaders/t.new.server.ts
Normal file
13
app/features/team/loaders/t.new.server.ts
Normal 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 };
|
||||
};
|
||||
@@ -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),
|
||||
);
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
60
app/features/team/routes/t.new.tsx
Normal file
60
app/features/team/routes/t.new.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user