Add friends widget

Closes #2914
This commit is contained in:
Kalle
2026-03-29 17:59:57 +03:00
parent ed1a8c311c
commit a1788ba89b
22 changed files with 191 additions and 24 deletions

View File

@@ -9,12 +9,15 @@ export function Pagination({
nextPage,
previousPage,
setPage,
compact,
}: {
currentPage: number;
pagesCount: number;
nextPage: () => void;
previousPage: () => void;
setPage: (page: number) => void;
/** Renders only the current page number with prev/next arrows */
compact?: boolean;
}) {
const pages = getPageNumbers(currentPage, pagesCount);
const [jumpToIndex, setJumpToIndex] = React.useState<number | null>(null);
@@ -30,30 +33,36 @@ export function Pagination({
>
<ChevronLeft size={18} />
</button>
{pages.map((page, index) =>
page.value === "..." ? (
<JumpToEllipsis
key={`ellipsis-${index}`}
isOpen={jumpToIndex === index}
pagesCount={pagesCount}
desktopOnly={page.desktopOnly}
mobileOnly={page.mobileOnly}
onOpen={() => setJumpToIndex(index)}
onClose={() => setJumpToIndex(null)}
onJump={(page) => {
setPage(page);
setJumpToIndex(null);
}}
/>
) : (
<PageButton
key={page.value}
page={page.value}
currentPage={currentPage}
desktopOnly={page.desktopOnly}
setPage={setPage}
/>
),
{compact ? (
<span className={clsx(styles.page, styles.pageActive)}>
{currentPage}
</span>
) : (
pages.map((page, index) =>
page.value === "..." ? (
<JumpToEllipsis
key={`ellipsis-${index}`}
isOpen={jumpToIndex === index}
pagesCount={pagesCount}
desktopOnly={page.desktopOnly}
mobileOnly={page.mobileOnly}
onOpen={() => setJumpToIndex(index)}
onClose={() => setJumpToIndex(null)}
onJump={(page) => {
setPage(page);
setJumpToIndex(null);
}}
/>
) : (
<PageButton
key={page.value}
page={page.value}
currentPage={currentPage}
desktopOnly={page.desktopOnly}
setPage={setPage}
/>
),
)
)}
<button
type="button"

View File

@@ -355,6 +355,34 @@ export async function countPendingSentRequests(
return result.count;
}
export async function findFriendsByUserId(userId: number) {
return db
.selectFrom("Friendship")
.innerJoin("User", (join) =>
join.on((eb) =>
eb.or([
eb.and([
eb("Friendship.userOneId", "=", userId),
eb("User.id", "=", eb.ref("Friendship.userTwoId")),
]),
eb.and([
eb("Friendship.userTwoId", "=", userId),
eb("User.id", "=", eb.ref("Friendship.userOneId")),
]),
]),
),
)
.where((eb) =>
eb.or([
eb("Friendship.userOneId", "=", userId),
eb("Friendship.userTwoId", "=", userId),
]),
)
.select([...COMMON_USER_FIELDS])
.orderBy("User.username", "asc")
.execute();
}
export async function findFriendIds(userId: number): Promise<number[]> {
const rows = await db
.selectFrom("Friendship")

View File

@@ -327,3 +327,23 @@
height: 48px;
object-fit: contain;
}
.friendsList {
display: flex;
flex-direction: column;
gap: var(--s-2);
}
.friendLink {
display: flex;
align-items: center;
gap: var(--s-1);
padding: var(--s-0-5) var(--s-1);
text-decoration: none;
color: var(--color-text);
border-radius: var(--radius-field);
&:hover {
background-color: var(--color-bg-higher);
}
}

View File

@@ -3,6 +3,7 @@ import { Link2 as LinkIcon } from "lucide-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import { Avatar } from "~/components/Avatar";
import { BuildCard } from "~/components/BuildCard";
import { SendouButton } from "~/components/elements/Button";
import { SendouPopover } from "~/components/elements/Popover";
@@ -12,11 +13,13 @@ import { DiscordIcon } from "~/components/icons/Discord";
import { TwitchIcon } from "~/components/icons/Twitch";
import { YouTubeIcon } from "~/components/icons/YouTube";
import { Markdown } from "~/components/Markdown";
import { Pagination } from "~/components/Pagination";
import { Placement } from "~/components/Placement";
import type { Tables } from "~/db/tables";
import { previewUrl } from "~/features/art/art-utils";
import { BadgeDisplay } from "~/features/badges/components/BadgeDisplay";
import { VodListing } from "~/features/vods/components/VodListing";
import { usePagination } from "~/hooks/usePagination";
import { useTimeFormat } from "~/hooks/useTimeFormat";
import type { GameBadgeId } from "~/modules/in-game-lists/game-badge-ids";
import type {
@@ -43,6 +46,7 @@ import {
tournamentOrganizationPage,
userArtPage,
userBuildsPage,
userPage,
userResultsPage,
userVodsPage,
} from "~/utils/urls";
@@ -245,6 +249,10 @@ export function Widget({
return widget.data.length === 0 ? null : (
<GameBadgesDisplay badgeIds={widget.data} />
);
case "friends":
return widget.data.length === 0 ? null : (
<FriendsWidget friends={widget.data} />
);
default:
assertUnreachable(widget);
}
@@ -865,6 +873,55 @@ function TierListWidget({ searchParams }: { searchParams: string }) {
);
}
const FRIENDS_PER_PAGE = 6;
function FriendsWidget({
friends,
}: {
friends: Extract<LoadedWidget, { id: "friends" }>["data"];
}) {
const {
itemsToDisplay,
currentPage,
pagesCount,
nextPage,
previousPage,
setPage,
everythingVisible,
} = usePagination({
items: friends,
pageSize: FRIENDS_PER_PAGE,
scrollToTop: false,
});
return (
<div className={styles.friendsList}>
{itemsToDisplay.map((friend) => (
<Link
key={friend.id}
to={userPage(friend)}
className={styles.friendLink}
>
<Avatar user={friend} size="xxs" />
{friend.username}
</Link>
))}
{!everythingVisible ? (
<div className="mt-4">
<Pagination
compact
currentPage={currentPage}
pagesCount={pagesCount}
nextPage={nextPage}
previousPage={previousPage}
setPage={setPage}
/>
</div>
) : null}
</div>
);
}
function GameBadgesDisplay({ badgeIds }: { badgeIds: string[] }) {
const { t } = useTranslation(["game-badges"]);

View File

@@ -1,6 +1,7 @@
import * as ArtRepository from "~/features/art/ArtRepository.server";
import * as BadgeRepository from "~/features/badges/BadgeRepository.server";
import * as BuildRepository from "~/features/builds/BuildRepository.server";
import * as FriendRepository from "~/features/friends/FriendRepository.server";
import * as LeaderboardRepository from "~/features/leaderboards/LeaderboardRepository.server";
import * as LFGRepository from "~/features/lfg/LFGRepository.server";
import { ordinalToSp } from "~/features/mmr/mmr-utils";
@@ -292,6 +293,9 @@ export const WIDGET_LOADERS = {
) => {
return settings.badgeIds;
},
friends: async (userId: number) => {
return FriendRepository.findFriendsByUserId(userId);
},
};
async function getTop500WeaponsByCategory(

View File

@@ -84,6 +84,7 @@ export const ALL_WIDGETS = {
defineWidget({ id: "badges-managed", slot: "main" }),
],
teams: [defineWidget({ id: "teams", slot: "side" })],
friends: [defineWidget({ id: "friends", slot: "side" })],
sendouq: [
defineWidget({ id: "peak-sp", slot: "side" }),
defineWidget({ id: "top-10-seasons", slot: "side" }),

View File

@@ -43,6 +43,7 @@
"widget.links": "",
"widget.game-badges": "",
"widget.game-badges-small": "",
"widget.friends": "",
"widget.tier-list": "",
"widget.link.all": "",
"widgets.edit": "",
@@ -66,6 +67,7 @@
"widgets.category.vods": "",
"widgets.category.builds": "",
"widgets.category.art": "",
"widgets.category.friends": "",
"widgets.category.game-badges": "",
"widgets.description.bio": "",
"widgets.description.bio-md": "",
@@ -109,6 +111,7 @@
"widgets.description.links": "",
"widgets.description.game-badges": "",
"widgets.description.game-badges-small": "",
"widgets.description.friends": "",
"widgets.description.tier-list": "",
"widgets.forms.bio": "",
"widgets.forms.bio.markdownSupport": "",

View File

@@ -43,6 +43,7 @@
"widget.links": "",
"widget.game-badges": "",
"widget.game-badges-small": "",
"widget.friends": "",
"widget.tier-list": "",
"widget.link.all": "",
"widgets.edit": "",
@@ -66,6 +67,7 @@
"widgets.category.vods": "",
"widgets.category.builds": "",
"widgets.category.art": "",
"widgets.category.friends": "",
"widgets.category.game-badges": "",
"widgets.description.bio": "",
"widgets.description.bio-md": "",
@@ -109,6 +111,7 @@
"widgets.description.links": "",
"widgets.description.game-badges": "",
"widgets.description.game-badges-small": "",
"widgets.description.friends": "",
"widgets.description.tier-list": "",
"widgets.forms.bio": "",
"widgets.forms.bio.markdownSupport": "",

View File

@@ -43,6 +43,7 @@
"widget.links": "Links",
"widget.game-badges": "Game Badges",
"widget.game-badges-small": "Game Badges",
"widget.friends": "Friends",
"widget.tier-list": "Featured Tier List",
"widget.link.all": "All",
"widgets.edit": "Edit Widgets",
@@ -66,6 +67,7 @@
"widgets.category.vods": "Videos",
"widgets.category.builds": "Builds",
"widgets.category.art": "Art",
"widgets.category.friends": "Friends",
"widgets.category.game-badges": "Game Badges",
"widgets.description.bio": "Share freeform information about yourself",
"widgets.description.bio-md": "Share freeform information about yourself with markdown formatting",
@@ -109,6 +111,7 @@
"widgets.description.links": "Share your custom social media and website links",
"widgets.description.game-badges": "Display up to {{max}} in-game badges on your profile",
"widgets.description.game-badges-small": "Display up to {{max}} in-game badges on your profile",
"widgets.description.friends": "Display your friends list",
"widgets.description.tier-list": "Display a tier list you have made",
"widgets.forms.bio": "Bio",
"widgets.forms.bio.markdownSupport": "Supports Markdown",

View File

@@ -43,6 +43,7 @@
"widget.links": "Enlaces",
"widget.game-badges": "",
"widget.game-badges-small": "",
"widget.friends": "",
"widget.tier-list": "Tier List destacada",
"widget.link.all": "Todos",
"widgets.edit": "Editar widgets",
@@ -66,6 +67,7 @@
"widgets.category.vods": "Vídeos",
"widgets.category.builds": "Builds",
"widgets.category.art": "Arte",
"widgets.category.friends": "",
"widgets.category.game-badges": "",
"widgets.description.bio": "Comparte información libre sobre ti",
"widgets.description.bio-md": "Comparte información libre sobre ti usando formato Markdown",
@@ -109,6 +111,7 @@
"widgets.description.links": "Comparte tus enlaces personalizados de webs y redes sociales",
"widgets.description.game-badges": "",
"widgets.description.game-badges-small": "",
"widgets.description.friends": "",
"widgets.description.tier-list": "Muestra una Tier List que hayas creado",
"widgets.forms.bio": "Biografía",
"widgets.forms.bio.markdownSupport": "Compatible con Markdown",

View File

@@ -43,6 +43,7 @@
"widget.links": "",
"widget.game-badges": "",
"widget.game-badges-small": "",
"widget.friends": "",
"widget.tier-list": "",
"widget.link.all": "",
"widgets.edit": "",
@@ -66,6 +67,7 @@
"widgets.category.vods": "",
"widgets.category.builds": "",
"widgets.category.art": "",
"widgets.category.friends": "",
"widgets.category.game-badges": "",
"widgets.description.bio": "",
"widgets.description.bio-md": "",
@@ -109,6 +111,7 @@
"widgets.description.links": "",
"widgets.description.game-badges": "",
"widgets.description.game-badges-small": "",
"widgets.description.friends": "",
"widgets.description.tier-list": "",
"widgets.forms.bio": "",
"widgets.forms.bio.markdownSupport": "",

View File

@@ -43,6 +43,7 @@
"widget.links": "",
"widget.game-badges": "",
"widget.game-badges-small": "",
"widget.friends": "",
"widget.tier-list": "",
"widget.link.all": "",
"widgets.edit": "",
@@ -66,6 +67,7 @@
"widgets.category.vods": "",
"widgets.category.builds": "",
"widgets.category.art": "",
"widgets.category.friends": "",
"widgets.category.game-badges": "",
"widgets.description.bio": "",
"widgets.description.bio-md": "",
@@ -109,6 +111,7 @@
"widgets.description.links": "",
"widgets.description.game-badges": "",
"widgets.description.game-badges-small": "",
"widgets.description.friends": "",
"widgets.description.tier-list": "",
"widgets.forms.bio": "",
"widgets.forms.bio.markdownSupport": "",

View File

@@ -43,6 +43,7 @@
"widget.links": "",
"widget.game-badges": "",
"widget.game-badges-small": "",
"widget.friends": "",
"widget.tier-list": "",
"widget.link.all": "",
"widgets.edit": "",
@@ -66,6 +67,7 @@
"widgets.category.vods": "",
"widgets.category.builds": "",
"widgets.category.art": "",
"widgets.category.friends": "",
"widgets.category.game-badges": "",
"widgets.description.bio": "",
"widgets.description.bio-md": "",
@@ -109,6 +111,7 @@
"widgets.description.links": "",
"widgets.description.game-badges": "",
"widgets.description.game-badges-small": "",
"widgets.description.friends": "",
"widgets.description.tier-list": "",
"widgets.forms.bio": "",
"widgets.forms.bio.markdownSupport": "",

View File

@@ -43,6 +43,7 @@
"widget.links": "",
"widget.game-badges": "",
"widget.game-badges-small": "",
"widget.friends": "",
"widget.tier-list": "",
"widget.link.all": "",
"widgets.edit": "",
@@ -66,6 +67,7 @@
"widgets.category.vods": "",
"widgets.category.builds": "",
"widgets.category.art": "",
"widgets.category.friends": "",
"widgets.category.game-badges": "",
"widgets.description.bio": "",
"widgets.description.bio-md": "",
@@ -109,6 +111,7 @@
"widgets.description.links": "",
"widgets.description.game-badges": "",
"widgets.description.game-badges-small": "",
"widgets.description.friends": "",
"widgets.description.tier-list": "",
"widgets.forms.bio": "",
"widgets.forms.bio.markdownSupport": "",

View File

@@ -43,6 +43,7 @@
"widget.links": "",
"widget.game-badges": "",
"widget.game-badges-small": "",
"widget.friends": "",
"widget.tier-list": "",
"widget.link.all": "",
"widgets.edit": "",
@@ -66,6 +67,7 @@
"widgets.category.vods": "",
"widgets.category.builds": "",
"widgets.category.art": "",
"widgets.category.friends": "",
"widgets.category.game-badges": "",
"widgets.description.bio": "",
"widgets.description.bio-md": "",
@@ -109,6 +111,7 @@
"widgets.description.links": "",
"widgets.description.game-badges": "",
"widgets.description.game-badges-small": "",
"widgets.description.friends": "",
"widgets.description.tier-list": "",
"widgets.forms.bio": "",
"widgets.forms.bio.markdownSupport": "",

View File

@@ -43,6 +43,7 @@
"widget.links": "",
"widget.game-badges": "",
"widget.game-badges-small": "",
"widget.friends": "",
"widget.tier-list": "",
"widget.link.all": "",
"widgets.edit": "",
@@ -66,6 +67,7 @@
"widgets.category.vods": "",
"widgets.category.builds": "",
"widgets.category.art": "",
"widgets.category.friends": "",
"widgets.category.game-badges": "",
"widgets.description.bio": "",
"widgets.description.bio-md": "",
@@ -109,6 +111,7 @@
"widgets.description.links": "",
"widgets.description.game-badges": "",
"widgets.description.game-badges-small": "",
"widgets.description.friends": "",
"widgets.description.tier-list": "",
"widgets.forms.bio": "",
"widgets.forms.bio.markdownSupport": "",

View File

@@ -43,6 +43,7 @@
"widget.links": "",
"widget.game-badges": "",
"widget.game-badges-small": "",
"widget.friends": "",
"widget.tier-list": "",
"widget.link.all": "",
"widgets.edit": "",
@@ -66,6 +67,7 @@
"widgets.category.vods": "",
"widgets.category.builds": "",
"widgets.category.art": "",
"widgets.category.friends": "",
"widgets.category.game-badges": "",
"widgets.description.bio": "",
"widgets.description.bio-md": "",
@@ -109,6 +111,7 @@
"widgets.description.links": "",
"widgets.description.game-badges": "",
"widgets.description.game-badges-small": "",
"widgets.description.friends": "",
"widgets.description.tier-list": "",
"widgets.forms.bio": "",
"widgets.forms.bio.markdownSupport": "",

View File

@@ -43,6 +43,7 @@
"widget.links": "",
"widget.game-badges": "",
"widget.game-badges-small": "",
"widget.friends": "",
"widget.tier-list": "",
"widget.link.all": "",
"widgets.edit": "",
@@ -66,6 +67,7 @@
"widgets.category.vods": "",
"widgets.category.builds": "",
"widgets.category.art": "",
"widgets.category.friends": "",
"widgets.category.game-badges": "",
"widgets.description.bio": "",
"widgets.description.bio-md": "",
@@ -109,6 +111,7 @@
"widgets.description.links": "",
"widgets.description.game-badges": "",
"widgets.description.game-badges-small": "",
"widgets.description.friends": "",
"widgets.description.tier-list": "",
"widgets.forms.bio": "",
"widgets.forms.bio.markdownSupport": "",

View File

@@ -43,6 +43,7 @@
"widget.links": "",
"widget.game-badges": "",
"widget.game-badges-small": "",
"widget.friends": "",
"widget.tier-list": "",
"widget.link.all": "",
"widgets.edit": "",
@@ -66,6 +67,7 @@
"widgets.category.vods": "",
"widgets.category.builds": "",
"widgets.category.art": "",
"widgets.category.friends": "",
"widgets.category.game-badges": "",
"widgets.description.bio": "",
"widgets.description.bio-md": "",
@@ -109,6 +111,7 @@
"widgets.description.links": "",
"widgets.description.game-badges": "",
"widgets.description.game-badges-small": "",
"widgets.description.friends": "",
"widgets.description.tier-list": "",
"widgets.forms.bio": "",
"widgets.forms.bio.markdownSupport": "",

View File

@@ -43,6 +43,7 @@
"widget.links": "",
"widget.game-badges": "",
"widget.game-badges-small": "",
"widget.friends": "",
"widget.tier-list": "",
"widget.link.all": "",
"widgets.edit": "",
@@ -66,6 +67,7 @@
"widgets.category.vods": "",
"widgets.category.builds": "",
"widgets.category.art": "",
"widgets.category.friends": "",
"widgets.category.game-badges": "",
"widgets.description.bio": "",
"widgets.description.bio-md": "",
@@ -109,6 +111,7 @@
"widgets.description.links": "",
"widgets.description.game-badges": "",
"widgets.description.game-badges-small": "",
"widgets.description.friends": "",
"widgets.description.tier-list": "",
"widgets.forms.bio": "",
"widgets.forms.bio.markdownSupport": "",

View File

@@ -43,6 +43,7 @@
"widget.links": "",
"widget.game-badges": "",
"widget.game-badges-small": "",
"widget.friends": "",
"widget.tier-list": "",
"widget.link.all": "",
"widgets.edit": "",
@@ -66,6 +67,7 @@
"widgets.category.vods": "",
"widgets.category.builds": "",
"widgets.category.art": "",
"widgets.category.friends": "",
"widgets.category.game-badges": "",
"widgets.description.bio": "",
"widgets.description.bio-md": "",
@@ -109,6 +111,7 @@
"widgets.description.links": "",
"widgets.description.game-badges": "",
"widgets.description.game-badges-small": "",
"widgets.description.friends": "",
"widgets.description.tier-list": "",
"widgets.forms.bio": "",
"widgets.forms.bio.markdownSupport": "",

View File

@@ -43,6 +43,7 @@
"widget.links": "",
"widget.game-badges": "",
"widget.game-badges-small": "",
"widget.friends": "",
"widget.tier-list": "",
"widget.link.all": "",
"widgets.edit": "",
@@ -66,6 +67,7 @@
"widgets.category.vods": "",
"widgets.category.builds": "",
"widgets.category.art": "",
"widgets.category.friends": "",
"widgets.category.game-badges": "",
"widgets.description.bio": "",
"widgets.description.bio-md": "",
@@ -109,6 +111,7 @@
"widgets.description.links": "",
"widgets.description.game-badges": "",
"widgets.description.game-badges-small": "",
"widgets.description.friends": "",
"widgets.description.tier-list": "",
"widgets.forms.bio": "",
"widgets.forms.bio.markdownSupport": "",