mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-05 20:56:13 -05:00
Mutual friends
This commit is contained in:
parent
b53e68d49a
commit
788061ca48
|
|
@ -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,
|
||||
|
|
|
|||
49
app/features/user-page/components/MutualFriends.module.css
Normal file
49
app/features/user-page/components/MutualFriends.module.css
Normal file
|
|
@ -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);
|
||||
}
|
||||
67
app/features/user-page/components/MutualFriends.tsx
Normal file
67
app/features/user-page/components/MutualFriends.tsx
Normal file
|
|
@ -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 (
|
||||
<div>
|
||||
<SendouPopover
|
||||
trigger={
|
||||
<SendouButton variant="minimal" size="small">
|
||||
<div className={styles.trigger}>
|
||||
<div className={styles.avatarStack}>
|
||||
{visibleFriends.map((friend) => (
|
||||
<Avatar
|
||||
key={friend.id}
|
||||
user={friend}
|
||||
size="xxs"
|
||||
className={styles.stackedAvatar}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{overflowCount > 0 ? (
|
||||
<span className={styles.overflow}>+{overflowCount}</span>
|
||||
) : null}
|
||||
<span>
|
||||
{t("user:mutualFriends.count", {
|
||||
count: mutualFriends.length,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</SendouButton>
|
||||
}
|
||||
>
|
||||
<div className={styles.list}>
|
||||
{mutualFriends.map((friend) => (
|
||||
<Link
|
||||
key={friend.id}
|
||||
to={userPage(friend)}
|
||||
className={styles.friendLink}
|
||||
>
|
||||
<Avatar user={friend} size="xxs" />
|
||||
{friend.username}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</SendouPopover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import type { LoaderFunctionArgs } from "react-router";
|
||||
import { getUser } from "~/features/auth/core/user.server";
|
||||
import * as FriendRepository from "~/features/friends/FriendRepository.server";
|
||||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
import type { SerializeFrom } from "~/utils/remix";
|
||||
import { notFoundIfFalsy } from "~/utils/remix.server";
|
||||
|
|
@ -20,11 +21,20 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
|
|||
params.identifier!,
|
||||
);
|
||||
|
||||
const mutualFriends =
|
||||
loggedInUser && loggedInUser.id !== user.id
|
||||
? await FriendRepository.findMutualFriends({
|
||||
loggedInUserId: loggedInUser.id,
|
||||
targetUserId: user.id,
|
||||
})
|
||||
: [];
|
||||
|
||||
return {
|
||||
user: {
|
||||
...user,
|
||||
},
|
||||
customTheme: user.customTheme,
|
||||
type: widgetsEnabled ? ("new" as const) : ("old" as const),
|
||||
mutualFriends,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import {
|
|||
teamPage,
|
||||
topSearchPlayerPage,
|
||||
} from "~/utils/urls";
|
||||
import { MutualFriends } from "../components/MutualFriends";
|
||||
import type { UserPageNavItem } from "../components/UserPageIconNav";
|
||||
import { UserPageIconNav } from "../components/UserPageIconNav";
|
||||
import { Widget } from "../components/Widget";
|
||||
|
|
@ -85,23 +86,26 @@ function NewUserInfoPage() {
|
|||
|
||||
return (
|
||||
<div className={newStyles.container}>
|
||||
<div className={newStyles.header}>
|
||||
<Avatar user={layoutData.user} size="xmd" />
|
||||
<div className={newStyles.userInfo}>
|
||||
<div className={newStyles.nameGroup}>
|
||||
<h1 className={newStyles.username}>{layoutData.user.username}</h1>
|
||||
<ProfileSubtitle
|
||||
inGameName={layoutData.user.inGameName}
|
||||
pronouns={layoutData.user.pronouns}
|
||||
plusTier={layoutData.user.plusTier}
|
||||
country={layoutData.user.country}
|
||||
language={i18n.language}
|
||||
/>
|
||||
<div className="stack sm">
|
||||
<div className={newStyles.header}>
|
||||
<Avatar user={layoutData.user} size="xmd" />
|
||||
<div className={newStyles.userInfo}>
|
||||
<div className={newStyles.nameGroup}>
|
||||
<h1 className={newStyles.username}>{layoutData.user.username}</h1>
|
||||
<ProfileSubtitle
|
||||
inGameName={layoutData.user.inGameName}
|
||||
pronouns={layoutData.user.pronouns}
|
||||
plusTier={layoutData.user.plusTier}
|
||||
country={layoutData.user.country}
|
||||
language={i18n.language}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={newStyles.desktopIconNav}>
|
||||
<UserPageIconNav items={navItems} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={newStyles.desktopIconNav}>
|
||||
<UserPageIconNav items={navItems} />
|
||||
</div>
|
||||
<MutualFriends mutualFriends={layoutData.mutualFriends} />
|
||||
</div>
|
||||
{isOwnPage ? (
|
||||
<div className={newStyles.editButtons}>
|
||||
|
|
@ -174,32 +178,37 @@ export function OldUserInfoPage() {
|
|||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.avatarContainer}>
|
||||
<Avatar user={layoutData.user} size="lg" className={styles.avatar} />
|
||||
<div>
|
||||
<h2 className={styles.name}>
|
||||
<div>{layoutData.user.username}</div>
|
||||
<div>
|
||||
{data.user.country ? (
|
||||
<Flag countryCode={data.user.country} tiny />
|
||||
) : null}
|
||||
</div>
|
||||
</h2>
|
||||
<TeamInfo />
|
||||
<div className="stack sm">
|
||||
<div className={styles.avatarContainer}>
|
||||
<Avatar user={layoutData.user} size="lg" className={styles.avatar} />
|
||||
<div>
|
||||
<h2 className={styles.name}>
|
||||
<div>{layoutData.user.username}</div>
|
||||
<div>
|
||||
{data.user.country ? (
|
||||
<Flag countryCode={data.user.country} tiny />
|
||||
) : null}
|
||||
</div>
|
||||
</h2>
|
||||
<TeamInfo />
|
||||
</div>
|
||||
<div className={styles.socials}>
|
||||
{data.user.twitch ? (
|
||||
<SocialLink type="twitch" identifier={data.user.twitch} />
|
||||
) : null}
|
||||
{data.user.youtubeId ? (
|
||||
<SocialLink type="youtube" identifier={data.user.youtubeId} />
|
||||
) : null}
|
||||
{data.user.battlefy ? (
|
||||
<SocialLink type="battlefy" identifier={data.user.battlefy} />
|
||||
) : null}
|
||||
{data.user.bsky ? (
|
||||
<SocialLink type="bsky" identifier={data.user.bsky} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.socials}>
|
||||
{data.user.twitch ? (
|
||||
<SocialLink type="twitch" identifier={data.user.twitch} />
|
||||
) : null}
|
||||
{data.user.youtubeId ? (
|
||||
<SocialLink type="youtube" identifier={data.user.youtubeId} />
|
||||
) : null}
|
||||
{data.user.battlefy ? (
|
||||
<SocialLink type="battlefy" identifier={data.user.battlefy} />
|
||||
) : null}
|
||||
{data.user.bsky ? (
|
||||
<SocialLink type="bsky" identifier={data.user.bsky} />
|
||||
) : null}
|
||||
<div className="stack items-center">
|
||||
<MutualFriends mutualFriends={layoutData.mutualFriends} />
|
||||
</div>
|
||||
</div>
|
||||
<ExtraInfos />
|
||||
|
|
|
|||
|
|
@ -193,5 +193,8 @@
|
|||
"builds.sorting.PUBLIC_BUILD": "offentlige sæt",
|
||||
"builds.sorting.PRIVATE_BUILD": "Private sæt",
|
||||
"commissions.open": "",
|
||||
"commissions.closed": ""
|
||||
"commissions.closed": "",
|
||||
"mutualFriends": "",
|
||||
"mutualFriends.count_one": "",
|
||||
"mutualFriends.count_other": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,5 +193,8 @@
|
|||
"builds.sorting.PUBLIC_BUILD": "",
|
||||
"builds.sorting.PRIVATE_BUILD": "",
|
||||
"commissions.open": "",
|
||||
"commissions.closed": ""
|
||||
"commissions.closed": "",
|
||||
"mutualFriends": "",
|
||||
"mutualFriends.count_one": "",
|
||||
"mutualFriends.count_other": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,5 +193,8 @@
|
|||
"builds.sorting.PUBLIC_BUILD": "Public build",
|
||||
"builds.sorting.PRIVATE_BUILD": "Private build",
|
||||
"commissions.open": "Open",
|
||||
"commissions.closed": "Closed"
|
||||
"commissions.closed": "Closed",
|
||||
"mutualFriends": "Mutual friends",
|
||||
"mutualFriends.count_one": "mutual friend",
|
||||
"mutualFriends.count_other": "mutual friends"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,5 +193,9 @@
|
|||
"builds.sorting.PUBLIC_BUILD": "",
|
||||
"builds.sorting.PRIVATE_BUILD": "",
|
||||
"commissions.open": "",
|
||||
"commissions.closed": ""
|
||||
"commissions.closed": "",
|
||||
"mutualFriends": "",
|
||||
"mutualFriends.count_one": "",
|
||||
"mutualFriends.count_many": "",
|
||||
"mutualFriends.count_other": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,5 +193,9 @@
|
|||
"builds.sorting.PUBLIC_BUILD": "Build público",
|
||||
"builds.sorting.PRIVATE_BUILD": "Build privado",
|
||||
"commissions.open": "",
|
||||
"commissions.closed": ""
|
||||
"commissions.closed": "",
|
||||
"mutualFriends": "",
|
||||
"mutualFriends.count_one": "",
|
||||
"mutualFriends.count_many": "",
|
||||
"mutualFriends.count_other": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,5 +193,9 @@
|
|||
"builds.sorting.PUBLIC_BUILD": "",
|
||||
"builds.sorting.PRIVATE_BUILD": "",
|
||||
"commissions.open": "",
|
||||
"commissions.closed": ""
|
||||
"commissions.closed": "",
|
||||
"mutualFriends": "",
|
||||
"mutualFriends.count_one": "",
|
||||
"mutualFriends.count_many": "",
|
||||
"mutualFriends.count_other": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,5 +193,9 @@
|
|||
"builds.sorting.PUBLIC_BUILD": "Build public",
|
||||
"builds.sorting.PRIVATE_BUILD": "Build privée",
|
||||
"commissions.open": "",
|
||||
"commissions.closed": ""
|
||||
"commissions.closed": "",
|
||||
"mutualFriends": "",
|
||||
"mutualFriends.count_one": "",
|
||||
"mutualFriends.count_many": "",
|
||||
"mutualFriends.count_other": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,5 +193,9 @@
|
|||
"builds.sorting.PUBLIC_BUILD": "",
|
||||
"builds.sorting.PRIVATE_BUILD": "",
|
||||
"commissions.open": "",
|
||||
"commissions.closed": ""
|
||||
"commissions.closed": "",
|
||||
"mutualFriends": "",
|
||||
"mutualFriends.count_one": "",
|
||||
"mutualFriends.count_two": "",
|
||||
"mutualFriends.count_other": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,5 +193,9 @@
|
|||
"builds.sorting.PUBLIC_BUILD": "Build pubblica",
|
||||
"builds.sorting.PRIVATE_BUILD": "Build privata",
|
||||
"commissions.open": "",
|
||||
"commissions.closed": ""
|
||||
"commissions.closed": "",
|
||||
"mutualFriends": "",
|
||||
"mutualFriends.count_one": "",
|
||||
"mutualFriends.count_many": "",
|
||||
"mutualFriends.count_other": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,5 +193,6 @@
|
|||
"builds.sorting.PUBLIC_BUILD": "公開ビルド",
|
||||
"builds.sorting.PRIVATE_BUILD": "非公開ビルド",
|
||||
"commissions.open": "",
|
||||
"commissions.closed": ""
|
||||
"commissions.closed": "",
|
||||
"mutualFriends": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,5 +193,6 @@
|
|||
"builds.sorting.PUBLIC_BUILD": "",
|
||||
"builds.sorting.PRIVATE_BUILD": "",
|
||||
"commissions.open": "",
|
||||
"commissions.closed": ""
|
||||
"commissions.closed": "",
|
||||
"mutualFriends": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,5 +193,8 @@
|
|||
"builds.sorting.PUBLIC_BUILD": "",
|
||||
"builds.sorting.PRIVATE_BUILD": "",
|
||||
"commissions.open": "",
|
||||
"commissions.closed": ""
|
||||
"commissions.closed": "",
|
||||
"mutualFriends": "",
|
||||
"mutualFriends.count_one": "",
|
||||
"mutualFriends.count_other": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,5 +193,10 @@
|
|||
"builds.sorting.PUBLIC_BUILD": "",
|
||||
"builds.sorting.PRIVATE_BUILD": "",
|
||||
"commissions.open": "",
|
||||
"commissions.closed": ""
|
||||
"commissions.closed": "",
|
||||
"mutualFriends": "",
|
||||
"mutualFriends.count_one": "",
|
||||
"mutualFriends.count_few": "",
|
||||
"mutualFriends.count_many": "",
|
||||
"mutualFriends.count_other": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,5 +193,9 @@
|
|||
"builds.sorting.PUBLIC_BUILD": "",
|
||||
"builds.sorting.PRIVATE_BUILD": "",
|
||||
"commissions.open": "",
|
||||
"commissions.closed": ""
|
||||
"commissions.closed": "",
|
||||
"mutualFriends": "",
|
||||
"mutualFriends.count_one": "",
|
||||
"mutualFriends.count_many": "",
|
||||
"mutualFriends.count_other": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,5 +193,10 @@
|
|||
"builds.sorting.PUBLIC_BUILD": "Публичные сборки",
|
||||
"builds.sorting.PRIVATE_BUILD": "Частные сборки",
|
||||
"commissions.open": "",
|
||||
"commissions.closed": ""
|
||||
"commissions.closed": "",
|
||||
"mutualFriends": "",
|
||||
"mutualFriends.count_one": "",
|
||||
"mutualFriends.count_few": "",
|
||||
"mutualFriends.count_many": "",
|
||||
"mutualFriends.count_other": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,5 +193,6 @@
|
|||
"builds.sorting.PUBLIC_BUILD": "公开配装",
|
||||
"builds.sorting.PRIVATE_BUILD": "私人配装",
|
||||
"commissions.open": "",
|
||||
"commissions.closed": ""
|
||||
"commissions.closed": "",
|
||||
"mutualFriends": ""
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user