From 788061ca48ce36ec96313c00d50246d0c717c662 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:44:19 +0200 Subject: [PATCH] Mutual friends --- .../friends/FriendRepository.server.ts | 66 ++++++++++++++ .../components/MutualFriends.module.css | 49 ++++++++++ .../user-page/components/MutualFriends.tsx | 67 ++++++++++++++ .../user-page/loaders/u.$identifier.server.ts | 10 +++ .../user-page/routes/u.$identifier.index.tsx | 89 ++++++++++--------- locales/da/user.json | 5 +- locales/de/user.json | 5 +- locales/en/user.json | 5 +- locales/es-ES/user.json | 6 +- locales/es-US/user.json | 6 +- locales/fr-CA/user.json | 6 +- locales/fr-EU/user.json | 6 +- locales/he/user.json | 6 +- locales/it/user.json | 6 +- locales/ja/user.json | 3 +- locales/ko/user.json | 3 +- locales/nl/user.json | 5 +- locales/pl/user.json | 7 +- locales/pt-BR/user.json | 6 +- locales/ru/user.json | 7 +- locales/zh/user.json | 3 +- 21 files changed, 310 insertions(+), 56 deletions(-) create mode 100644 app/features/user-page/components/MutualFriends.module.css create mode 100644 app/features/user-page/components/MutualFriends.tsx diff --git a/app/features/friends/FriendRepository.server.ts b/app/features/friends/FriendRepository.server.ts index baa501dfb..17f812958 100644 --- a/app/features/friends/FriendRepository.server.ts +++ b/app/features/friends/FriendRepository.server.ts @@ -255,6 +255,72 @@ export async function deleteFriendRequestByReceiver({ .execute(); } +export async function findMutualFriends({ + loggedInUserId, + targetUserId, +}: { + loggedInUserId: number; + targetUserId: number; +}) { + return db + .selectFrom("Friendship as f1") + .innerJoin("Friendship as f2", (join) => + join.on((eb) => + eb.and([ + eb( + eb + .case() + .when("f1.userOneId", "=", loggedInUserId) + .then(eb.ref("f1.userTwoId")) + .else(eb.ref("f1.userOneId")) + .end(), + "=", + eb + .case() + .when("f2.userOneId", "=", targetUserId) + .then(eb.ref("f2.userTwoId")) + .else(eb.ref("f2.userOneId")) + .end(), + ), + ]), + ), + ) + .innerJoin("User", (join) => + join.on((eb) => + eb( + "User.id", + "=", + eb + .case() + .when("f1.userOneId", "=", loggedInUserId) + .then(eb.ref("f1.userTwoId")) + .else(eb.ref("f1.userOneId")) + .end(), + ), + ), + ) + .where((eb) => + eb.or([ + eb("f1.userOneId", "=", loggedInUserId), + eb("f1.userTwoId", "=", loggedInUserId), + ]), + ) + .where((eb) => + eb.or([ + eb("f2.userOneId", "=", targetUserId), + eb("f2.userTwoId", "=", targetUserId), + ]), + ) + .select([ + "User.id", + "User.username", + "User.discordId", + "User.discordAvatar", + "User.customUrl", + ]) + .execute(); +} + export async function findFriendship({ userOneId, userTwoId, diff --git a/app/features/user-page/components/MutualFriends.module.css b/app/features/user-page/components/MutualFriends.module.css new file mode 100644 index 000000000..485c966f0 --- /dev/null +++ b/app/features/user-page/components/MutualFriends.module.css @@ -0,0 +1,49 @@ +.trigger { + display: flex; + align-items: center; + + span { + color: var(--color-text-high); + } +} + +.avatarStack { + display: flex; + align-items: center; +} + +.stackedAvatar { + border-radius: 50%; + border: 2px solid var(--color-bg); +} + +.stackedAvatar:not(:first-child) { + margin-inline-start: -8px; +} + +.overflow { + font-size: var(--font-xs); + margin-inline: var(--s-1); +} + +.centered { + display: flex; + justify-content: center; +} + +.list { + display: flex; + flex-direction: column; + gap: var(--s-0-5); + max-height: 300px; + overflow-y: auto; +} + +.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); +} diff --git a/app/features/user-page/components/MutualFriends.tsx b/app/features/user-page/components/MutualFriends.tsx new file mode 100644 index 000000000..13baf402d --- /dev/null +++ b/app/features/user-page/components/MutualFriends.tsx @@ -0,0 +1,67 @@ +import { useTranslation } from "react-i18next"; +import { Link } from "react-router"; +import { Avatar } from "~/components/Avatar"; +import { SendouButton } from "~/components/elements/Button"; +import { SendouPopover } from "~/components/elements/Popover"; +import { userPage } from "~/utils/urls"; +import type { UserPageLoaderData } from "../loaders/u.$identifier.server"; +import styles from "./MutualFriends.module.css"; + +const MAX_VISIBLE_AVATARS = 5; + +export function MutualFriends({ + mutualFriends, +}: { + mutualFriends: UserPageLoaderData["mutualFriends"]; +}) { + const { t } = useTranslation(["user"]); + + if (mutualFriends.length === 0) return null; + + const visibleFriends = mutualFriends.slice(0, MAX_VISIBLE_AVATARS); + const overflowCount = mutualFriends.length - MAX_VISIBLE_AVATARS; + + return ( +