Add main team quick access

This commit is contained in:
Kalle
2026-09-05 07:11:40 +03:00
parent ee258f83e7
commit b6e202c277
27 changed files with 125 additions and 4 deletions

View File

@@ -265,14 +265,15 @@
}
.youPanelUserRow {
display: grid;
grid-template-columns: 1fr auto;
display: flex;
align-items: center;
padding: var(--s-1) var(--s-2);
}
.youPanelUser {
display: flex;
flex: 1;
min-width: 0;
align-items: center;
gap: var(--s-3);
padding: var(--s-2);
@@ -311,6 +312,11 @@
}
}
.youPanelTeamAvatar {
border-radius: 50%;
object-fit: cover;
}
.panelSectionLink {
display: flex;
align-items: center;

View File

@@ -32,6 +32,7 @@ import {
SENDOU_INK_BASE_URL,
SETTINGS_PAGE,
SUPPORT_PAGE,
teamPage,
userPage,
} from "~/utils/urls";
import { Avatar } from "./Avatar";
@@ -580,6 +581,26 @@ function YouPanel({
<Avatar user={user} size="sm" />
<span className={styles.youPanelUsername}>{user.username}</span>
</Link>
{user.team ? (
<Link
to={teamPage(user.team.customUrl)}
className={styles.youPanelSettingsButton}
onClick={onClose}
aria-label={t("common:header.myTeam")}
>
{user.team.avatarUrl ? (
<img
src={user.team.avatarUrl}
alt=""
className={styles.youPanelTeamAvatar}
width={22}
height={22}
/>
) : (
<Image path={navIconUrl("t")} alt="" width={22} height={22} />
)}
</Link>
) : null}
<Link
to={SETTINGS_PAGE}
className={styles.youPanelSettingsButton}

View File

@@ -395,6 +395,11 @@
}
}
.sideNavFooterTeamAvatar {
border-radius: 50%;
object-fit: cover;
}
.sideNavFooterNotification {
position: relative;
}

View File

@@ -38,8 +38,10 @@ import type { Breadcrumb, SendouRouteHandle } from "~/utils/remix.server";
import {
EVENTS_PAGE,
FRIENDS_PAGE,
navIconUrl,
PLANNER_URL,
SETTINGS_PAGE,
teamPage,
userPage,
} from "~/utils/urls";
import { Avatar, generateIdenticon } from "../Avatar";
@@ -663,6 +665,26 @@ function SideNavUserPanel() {
<span className={styles.sideNavFooterUsername}>{user.username}</span>
</Link>
<div className={styles.sideNavFooterActions}>
{user.team ? (
<Link
to={teamPage(user.team.customUrl)}
className={styles.sideNavFooterButton}
aria-label={t("header.myTeam")}
title={t("header.myTeam")}
>
{user.team.avatarUrl ? (
<img
src={user.team.avatarUrl}
alt=""
className={styles.sideNavFooterTeamAvatar}
width={22}
height={22}
/>
) : (
<Image path={navIconUrl("t")} alt="" width={22} height={22} />
)}
</Link>
) : null}
{notifications ? (
<div
className={styles.sideNavFooterNotification}

View File

@@ -58,7 +58,7 @@ export function validateInviteCode({
inviteCode: string;
realInviteCode: string;
team: TeamRepository.findByCustomUrl;
user?: { id: number; team?: { name: string } };
user?: { id: number };
reachedTeamCountLimit: boolean;
}) {
if (inviteCode.length !== SHORT_NANOID_LENGTH) {

View File

@@ -29,6 +29,7 @@ import {
concatUserSubmittedImagePrefix,
customAvatarUrl,
jsonArrayFrom,
jsonObjectFrom,
tournamentLogoOrNull,
userByIdentifierQuery,
userProfileWeapons,
@@ -438,6 +439,24 @@ export async function findLeanById(id: number) {
.orderBy("UserFriendCode.createdAt", "desc")
.limit(1)
.as("friendCode"),
jsonObjectFrom(
eb
.selectFrom("TeamMember")
.innerJoin("Team", "Team.id", "TeamMember.teamId")
.leftJoin(
"UserSubmittedImage",
"UserSubmittedImage.id",
"Team.avatarImgId",
)
.select(({ ref }) => [
"Team.name",
"Team.customUrl",
concatUserSubmittedImagePrefix(ref("UserSubmittedImage.url")).as(
"avatarUrl",
),
])
.where("TeamMember.userId", "=", id),
).as("team"),
])
.executeTakeFirst();

View File

@@ -170,6 +170,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
plusTier: user.plusTier,
roles: user.roles,
createdAt: user.createdAt,
team: user.team,
}
: undefined,
customTheme: isSupporter(user) ? user?.customTheme : undefined,

View File

@@ -0,0 +1,4 @@
---
type: feature
---
Quick access to your team's page: your main team's avatar is now shown next to the notification bell in the side navigation (and in the "You" panel on mobile)

View File

@@ -1,5 +1,5 @@
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { SENDOUQ_PAGE } from "~/utils/urls";
import { SENDOUQ_PAGE, teamPage } from "~/utils/urls";
import {
expect,
impersonate,
@@ -9,6 +9,7 @@ import {
test,
} from "./helpers/playwright";
import { MobileNav } from "./pages/layout/mobile-nav";
import { SideNav } from "./pages/layout/side-nav";
import { TopNavMenus } from "./pages/layout/top-nav-menus";
test.describe("Navigation", () => {
@@ -62,6 +63,28 @@ test.describe("Navigation", () => {
await expect(mobileNav.menuLink("SendouQ")).not.toBeVisible();
});
test("my team shortcut navigates to the team page on desktop and mobile", async ({
page,
factories,
}) => {
const team = await factories.TeamFactory.create({
memberUserIds: [NZAP_TEST_ID],
});
await impersonate(page, NZAP_TEST_ID);
await navigate({ page, url: "/" });
await new SideNav(page).locators.footerTeamLink.click();
await expect(page).toHaveURL(teamPage(team.customUrl));
await navigate({ page, url: "/" });
await page.setViewportSize(MOBILE_VIEWPORT);
const mobileNav = new MobileNav(page);
await mobileNav.openPanel("you");
await mobileNav.locators.youPanelTeamLink.click();
await expect(page).toHaveURL(teamPage(team.customUrl));
});
test("tablet navigation", async ({ page }) => {
await page.setViewportSize(TABLET_VIEWPORT);
await impersonate(page, NZAP_TEST_ID);

View File

@@ -33,6 +33,9 @@ export class MobileNav {
youPanelSettingsLink: this.openPanelDialog.getByRole("link", {
name: "Settings",
}),
youPanelTeamLink: this.openPanelDialog.getByRole("link", {
name: "My team",
}),
friendItems: this.openPanelDialog.locator("button[class*='listButton']"),
};
}

View File

@@ -21,6 +21,7 @@ export class SideNav {
name: "Log in via Discord",
}),
footerUsername: this.root.locator("[class*='sideNavFooterUsername']"),
footerTeamLink: this.root.getByRole("link", { name: "My team" }),
collapseButton: page.getByTestId("sidenav-collapse-button"),
modalTrigger: page.getByTestId("sidenav-modal-trigger"),
siteLogoLink: this.root.locator("a[class*='siteLogo']"),

View File

@@ -44,6 +44,7 @@
"header.adder.calendarEvent": "",
"header.adder.build": "",
"header.adder.team": "",
"header.myTeam": "",
"header.adder.scrimPost": "",
"header.adder.association": "",
"header.adder.lfgPost": "",

View File

@@ -44,6 +44,7 @@
"header.adder.calendarEvent": "",
"header.adder.build": "",
"header.adder.team": "",
"header.myTeam": "",
"header.adder.scrimPost": "",
"header.adder.association": "",
"header.adder.lfgPost": "",

View File

@@ -44,6 +44,7 @@
"header.adder.calendarEvent": "Calendar event",
"header.adder.build": "Build",
"header.adder.team": "Team",
"header.myTeam": "My team",
"header.adder.scrimPost": "Scrim post",
"header.adder.association": "Association",
"header.adder.lfgPost": "LFG post",

View File

@@ -44,6 +44,7 @@
"header.adder.calendarEvent": "Evento de calendario",
"header.adder.build": "Build",
"header.adder.team": "Equipo",
"header.myTeam": "",
"header.adder.scrimPost": "Publicación de scrim",
"header.adder.association": "Asociación",
"header.adder.lfgPost": "Publicación de LFG",

View File

@@ -44,6 +44,7 @@
"header.adder.calendarEvent": "Evento de calendario",
"header.adder.build": "Build",
"header.adder.team": "Equipo",
"header.myTeam": "",
"header.adder.scrimPost": "Publicación de scrim",
"header.adder.association": "Asociación",
"header.adder.lfgPost": "Publicación de LFG",

View File

@@ -44,6 +44,7 @@
"header.adder.calendarEvent": "",
"header.adder.build": "",
"header.adder.team": "",
"header.myTeam": "",
"header.adder.scrimPost": "",
"header.adder.association": "",
"header.adder.lfgPost": "",

View File

@@ -44,6 +44,7 @@
"header.adder.calendarEvent": "Calendrier d'évenement",
"header.adder.build": "set",
"header.adder.team": "Equipe",
"header.myTeam": "",
"header.adder.scrimPost": "Poste scrim",
"header.adder.association": "Association",
"header.adder.lfgPost": "Post LFG",

View File

@@ -44,6 +44,7 @@
"header.adder.calendarEvent": "אירוע בלוח שנה",
"header.adder.build": "ערכה",
"header.adder.team": "צוות",
"header.myTeam": "",
"header.adder.scrimPost": "",
"header.adder.association": "",
"header.adder.lfgPost": "",

View File

@@ -44,6 +44,7 @@
"header.adder.calendarEvent": "Evento calendario",
"header.adder.build": "Build",
"header.adder.team": "Team",
"header.myTeam": "",
"header.adder.scrimPost": "",
"header.adder.association": "",
"header.adder.lfgPost": "Post LFG",

View File

@@ -44,6 +44,7 @@
"header.adder.calendarEvent": "イベント",
"header.adder.build": "ギア構成",
"header.adder.team": "チーム",
"header.myTeam": "",
"header.adder.scrimPost": "対抗戦募集",
"header.adder.association": "コミュニティ",
"header.adder.lfgPost": "メンバー探し",

View File

@@ -44,6 +44,7 @@
"header.adder.calendarEvent": "",
"header.adder.build": "",
"header.adder.team": "",
"header.myTeam": "",
"header.adder.scrimPost": "",
"header.adder.association": "",
"header.adder.lfgPost": "",

View File

@@ -44,6 +44,7 @@
"header.adder.calendarEvent": "",
"header.adder.build": "",
"header.adder.team": "",
"header.myTeam": "",
"header.adder.scrimPost": "",
"header.adder.association": "",
"header.adder.lfgPost": "",

View File

@@ -44,6 +44,7 @@
"header.adder.calendarEvent": "",
"header.adder.build": "",
"header.adder.team": "",
"header.myTeam": "",
"header.adder.scrimPost": "",
"header.adder.association": "",
"header.adder.lfgPost": "",

View File

@@ -44,6 +44,7 @@
"header.adder.calendarEvent": "",
"header.adder.build": "",
"header.adder.team": "",
"header.myTeam": "",
"header.adder.scrimPost": "",
"header.adder.association": "",
"header.adder.lfgPost": "",

View File

@@ -44,6 +44,7 @@
"header.adder.calendarEvent": "Календарное событие",
"header.adder.build": "Сборка",
"header.adder.team": "Команда",
"header.myTeam": "",
"header.adder.scrimPost": "Скрим пост",
"header.adder.association": "Ассоциация",
"header.adder.lfgPost": "LFG пост",

View File

@@ -44,6 +44,7 @@
"header.adder.calendarEvent": "赛事日程",
"header.adder.build": "配装",
"header.adder.team": "队伍",
"header.myTeam": "",
"header.adder.scrimPost": "对抗战招募帖",
"header.adder.association": "群组",
"header.adder.lfgPost": "组队招募帖",