Skip to content

Commit ce2d215

Browse files
authored
feat(webapp): add squad top members links (#5831)
1 parent 064611f commit ce2d215

12 files changed

Lines changed: 490 additions & 70 deletions

File tree

packages/shared/src/components/errors/SquadLoading.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,45 @@ const Actions = ({ className }: HTMLAttributes<HTMLDivElement>) => (
3232
</FlexRow>
3333
);
3434

35+
const MemberCard = ({
36+
className,
37+
showBadge = true,
38+
}: HTMLAttributes<HTMLDivElement> & { showBadge?: boolean }) => (
39+
<FlexRow
40+
className={classNames(
41+
'items-center gap-2 rounded-10 border border-border-subtlest-tertiary p-2',
42+
className,
43+
)}
44+
>
45+
<PlaceholderElement className="h-10 w-10 rounded-full" />
46+
<FlexCol className="gap-1">
47+
<RectangleElement className="h-4 w-20" />
48+
{showBadge && <RectangleElement className="h-4 w-14" />}
49+
</FlexCol>
50+
</FlexRow>
51+
);
52+
53+
const MemberRow = ({
54+
titleWidthClassName,
55+
className,
56+
showOverflow = false,
57+
showBadge = true,
58+
}: HTMLAttributes<HTMLDivElement> & {
59+
titleWidthClassName: string;
60+
showOverflow?: boolean;
61+
showBadge?: boolean;
62+
}) => (
63+
<FlexCol className={classNames('w-full items-start', className)}>
64+
<RectangleElement className={classNames('h-4', titleWidthClassName)} />
65+
<FlexRow className="mt-2 w-full items-center gap-3 overflow-hidden">
66+
<MemberCard showBadge={showBadge} />
67+
<MemberCard showBadge={showBadge} />
68+
<MemberCard showBadge={showBadge} />
69+
{showOverflow && <RectangleElement className="h-12 w-12 rounded-12" />}
70+
</FlexRow>
71+
</FlexCol>
72+
);
73+
3574
function SquadLoading({
3675
squad,
3776
sidebarRendered,
@@ -59,6 +98,19 @@ function SquadLoading({
5998
<RectangleElement className="h-12 w-12 tablet:w-32" />
6099
<RectangleElement className="hidden h-12 w-32 tablet:flex" />
61100
</FlexRow>
101+
<MemberRow
102+
titleWidthClassName="mt-6 w-24"
103+
className="max-w-[38.5rem]"
104+
showOverflow
105+
/>
106+
{squad?.public && (
107+
<MemberRow
108+
titleWidthClassName="mt-4 w-24"
109+
className="max-w-[38.5rem]"
110+
showOverflow
111+
showBadge={false}
112+
/>
113+
)}
62114
<RectangleElement className="relative bottom-0 mt-8 flex h-16 w-full max-w-[30.25rem] flex-col pt-8 tablet:absolute tablet:translate-y-1/2 tablet:flex-row tablet:p-0 laptop:mt-6 laptop:max-w-[38.25rem] laptopL:px-0" />
63115
</FlexCol>
64116
<ColumnContainer className="relative max-h-page w-full overflow-hidden px-16 pt-7">

packages/shared/src/components/modals/common.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ const PrivilegedMemberModal = dynamic(
120120
/* webpackChunkName: "privilegedMembersModal" */ './squads/PrivilegedMembersModal'
121121
),
122122
);
123+
const TopMembersModal = dynamic(
124+
() =>
125+
import(
126+
/* webpackChunkName: "topMembersModal" */ './squads/TopMembersModal'
127+
),
128+
);
123129

124130
const BookmarkReminderModal = dynamic(
125131
() =>
@@ -481,6 +487,7 @@ export const modals = {
481487
[LazyModal.MarketingCta]: MarketingCtaModal,
482488
[LazyModal.Share]: ShareModal,
483489
[LazyModal.PrivilegedMembers]: PrivilegedMemberModal,
490+
[LazyModal.TopMembers]: TopMembersModal,
484491
[LazyModal.BookmarkReminder]: BookmarkReminderModal,
485492
[LazyModal.RecoverStreak]: StreakRecoverModal,
486493
[LazyModal.SlackIntegration]: SlackIntegrationModal,

packages/shared/src/components/modals/common/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export enum LazyModal {
4646
MarketingCta = 'marketingCta',
4747
Share = 'share',
4848
PrivilegedMembers = 'privilegedMembers',
49+
TopMembers = 'topMembers',
4950
BookmarkReminder = 'bookmarkReminder',
5051
SlackIntegration = 'slackIntegration',
5152
ReportSource = 'reportSource',

packages/shared/src/components/modals/squads/PrivilegedMembersModal.tsx

Lines changed: 12 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
11
import type { ReactElement } from 'react';
22
import React from 'react';
3-
import Link from '../../utilities/Link';
43
import type { ModalProps } from '../common/Modal';
5-
import { Modal } from '../common/Modal';
64
import type { Source } from '../../../graphql/sources';
7-
import { UserShortInfo } from '../../profile/UserShortInfo';
8-
import { Origin } from '../../../lib/log';
9-
import { useSquad } from '../../../hooks';
10-
11-
import { generateQueryKey, RequestKey } from '../../../lib/query';
12-
import { useAuthContext } from '../../../contexts/AuthContext';
13-
import { useSourceContentPreferenceMutationSubscription } from '../../../hooks/contentPreference/useSourceContentPreferenceMutationSubscription';
5+
import { SquadUsersModal } from './SquadUsersModal';
146

157
export interface PrivilegedMembersModalProps
168
extends Omit<ModalProps, 'children'> {
@@ -21,30 +13,18 @@ function PrivilegedMembersModal({
2113
source,
2214
...props
2315
}: PrivilegedMembersModalProps): ReactElement {
24-
const { user: loggedUser } = useAuthContext();
25-
const { squad } = useSquad({ handle: source.handle });
26-
27-
useSourceContentPreferenceMutationSubscription({
28-
queryKey: generateQueryKey(RequestKey.Squad, loggedUser, source.handle),
29-
});
30-
3116
return (
32-
<Modal kind={Modal.Kind.FlexibleCenter} size={Modal.Size.Medium} {...props}>
33-
<Modal.Header title="Moderated by" />
34-
<Modal.Body className="!p-0">
35-
{squad?.privilegedMembers?.map(({ user, role }) => (
36-
<Link key={user.username} href={user.permalink}>
37-
<UserShortInfo
38-
tag="a"
39-
href={user.permalink}
40-
user={{ ...user, role }}
41-
showFollow
42-
origin={Origin.SquadMembersList}
43-
/>
44-
</Link>
45-
))}
46-
</Modal.Body>
47-
</Modal>
17+
<SquadUsersModal
18+
{...props}
19+
source={source}
20+
title="Moderated by"
21+
getUsers={(squad) =>
22+
squad?.privilegedMembers?.map(({ user, role }) => ({
23+
...user,
24+
role,
25+
})) ?? []
26+
}
27+
/>
4828
);
4929
}
5030

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { ReactElement } from 'react';
2+
import React from 'react';
3+
import type { UserShortProfile } from '../../../lib/user';
4+
import type { Source, SourceMemberRole, Squad } from '../../../graphql/sources';
5+
import { UserShortInfo } from '../../profile/UserShortInfo';
6+
import type { ModalProps } from '../common/Modal';
7+
import { Modal } from '../common/Modal';
8+
import { Origin } from '../../../lib/log';
9+
import { useSquad } from '../../../hooks';
10+
import { useAuthContext } from '../../../contexts/AuthContext';
11+
import { useSourceContentPreferenceMutationSubscription } from '../../../hooks/contentPreference/useSourceContentPreferenceMutationSubscription';
12+
import { generateQueryKey, RequestKey } from '../../../lib/query';
13+
14+
export interface SquadUsersModalProps extends Omit<ModalProps, 'children'> {
15+
source: Pick<Source, 'handle'>;
16+
title: string;
17+
getUsers: (
18+
squad: Squad | undefined,
19+
) => Array<UserShortProfile & { role?: SourceMemberRole }>;
20+
}
21+
22+
export function SquadUsersModal({
23+
source,
24+
title,
25+
getUsers,
26+
...props
27+
}: SquadUsersModalProps): ReactElement {
28+
const { user: loggedUser } = useAuthContext();
29+
const { squad } = useSquad({ handle: source.handle });
30+
31+
useSourceContentPreferenceMutationSubscription({
32+
queryKey: generateQueryKey(RequestKey.Squad, loggedUser, source.handle),
33+
});
34+
35+
const users = getUsers(squad);
36+
37+
return (
38+
<Modal kind={Modal.Kind.FlexibleCenter} size={Modal.Size.Medium} {...props}>
39+
<Modal.Header title={title} />
40+
<Modal.Body className="!p-0">
41+
{users.map((user) => (
42+
<UserShortInfo
43+
key={user.id}
44+
tag="a"
45+
href={user.permalink}
46+
user={user}
47+
showFollow
48+
origin={Origin.SquadMembersList}
49+
/>
50+
))}
51+
</Modal.Body>
52+
</Modal>
53+
);
54+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { ReactElement } from 'react';
2+
import React from 'react';
3+
import type { ModalProps } from '../common/Modal';
4+
import type { Source } from '../../../graphql/sources';
5+
import { SquadUsersModal } from './SquadUsersModal';
6+
7+
export interface TopMembersModalProps extends Omit<ModalProps, 'children'> {
8+
source: Pick<Source, 'handle'>;
9+
}
10+
11+
function TopMembersModal({
12+
source,
13+
...props
14+
}: TopMembersModalProps): ReactElement {
15+
return (
16+
<SquadUsersModal
17+
{...props}
18+
source={source}
19+
title="Top members"
20+
getUsers={(squad) => squad?.topMembers ?? []}
21+
/>
22+
);
23+
}
24+
25+
export default TopMembersModal;

packages/shared/src/components/squads/Members/PrivilegedMemberItem.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import type { ReactElement } from 'react';
22
import React from 'react';
33
import { ProfileImageSize, ProfilePicture } from '../../ProfilePicture';
4-
import type { SourceMember } from '../../../graphql/sources';
5-
import { ProfileTooltip } from '../../profile/ProfileTooltip';
4+
import type { SourceMember, SourceMemberRole } from '../../../graphql/sources';
5+
import { SourceMemberRole as SourceMemberRoleEnum } from '../../../graphql/sources';
66
import { ProfileLink } from '../../profile/ProfileLink';
7+
import { ProfileTooltip } from '../../profile/ProfileTooltip';
78
import UserBadge from '../../UserBadge';
8-
import { getRoleName } from '../../utilities';
99

1010
interface PrivilegedMemberItemProps {
11-
member: SourceMember;
11+
user: SourceMember['user'];
12+
badge?: string;
13+
role?: SourceMemberRole;
1214
}
1315

1416
export function PrivilegedMemberItem({
15-
member: { user, role },
17+
user,
18+
badge,
19+
role = SourceMemberRoleEnum.Member,
1620
}: PrivilegedMemberItemProps): ReactElement {
1721
return (
1822
<ProfileTooltip userId={user.id} tooltip={{ placement: 'bottom' }}>
@@ -25,9 +29,11 @@ export function PrivilegedMemberItem({
2529
<span className="flex truncate text-text-tertiary typo-subhead">
2630
{user.name}
2731
</span>
28-
<UserBadge className="w-fit" role={role}>
29-
{getRoleName(role)}
30-
</UserBadge>
32+
{badge && (
33+
<UserBadge className="w-fit" role={role}>
34+
{badge}
35+
</UserBadge>
36+
)}
3137
</div>
3238
</ProfileLink>
3339
</ProfileTooltip>

packages/shared/src/components/squads/SquadPageHeader.tsx

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import type { ReactElement } from 'react';
22
import React from 'react';
33
import classNames from 'classnames';
44
import type { BasicSourceMember, Squad } from '../../graphql/sources';
5-
import { SourcePermissions } from '../../graphql/sources';
5+
import { SourceMemberRole, SourcePermissions } from '../../graphql/sources';
66
import { SquadHeaderBar } from './SquadHeaderBar';
77
import { SquadImage } from './SquadImage';
8-
import { FlexCentered, FlexCol } from '../utilities';
8+
import { FlexCentered, FlexCol, getRoleName } from '../utilities';
99
import SharePostBar from './SharePostBar';
1010
import { verifyPermission } from '../../graphql/squads';
1111
import { Button, ButtonColor, ButtonVariant } from '../buttons/Button';
@@ -57,6 +57,8 @@ export function SquadPageHeader({
5757
? formatMonthYearOnly(squad.createdAt)
5858
: null;
5959
const privilegedLength = squad.privilegedMembers?.length || 0;
60+
const topMembers = squad.topMembers ?? [];
61+
const topMembersLength = topMembers.length;
6062
const isMobile = useViewSize(ViewSize.MobileL);
6163
const listMax = isMobile
6264
? MAX_VISIBLE_PRIVILEGED_MEMBERS_MOBILE
@@ -164,7 +166,12 @@ export function SquadPageHeader({
164166
</Typography>
165167
<div className="mt-2 flex flex-row items-center gap-3">
166168
{squad.privilegedMembers?.slice(0, listMax).map((member) => (
167-
<PrivilegedMemberItem key={member.user.id} member={member} />
169+
<PrivilegedMemberItem
170+
key={member.user.id}
171+
user={member.user}
172+
role={member.role}
173+
badge={getRoleName(member.role)}
174+
/>
168175
))}
169176
{privilegedLength > listMax && (
170177
<Button
@@ -181,6 +188,42 @@ export function SquadPageHeader({
181188
</Button>
182189
)}
183190
</div>
191+
{topMembers.length > 0 && (
192+
<>
193+
<Typography
194+
bold
195+
className="mt-4"
196+
color={TypographyColor.Tertiary}
197+
tag={TypographyTag.Span}
198+
type={TypographyType.Caption1}
199+
>
200+
Top members
201+
</Typography>
202+
<div className="mt-2 flex flex-row items-center gap-3">
203+
{topMembers.slice(0, listMax).map((member) => (
204+
<PrivilegedMemberItem
205+
key={member.id}
206+
user={member}
207+
role={SourceMemberRole.Member}
208+
/>
209+
))}
210+
{topMembersLength > listMax && (
211+
<Button
212+
variant={ButtonVariant.Tertiary}
213+
className="aspect-square border border-border-subtlest-tertiary"
214+
onClick={() =>
215+
openModal({
216+
type: LazyModal.TopMembers,
217+
props: { source: squad },
218+
})
219+
}
220+
>
221+
+{topMembersLength - listMax}
222+
</Button>
223+
)}
224+
</div>
225+
</>
226+
)}
184227
<div className={classNames('w-full', MAX_WIDTH)}>
185228
<SquadStack squad={squad} />
186229
</div>

packages/shared/src/graphql/sources.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export interface Squad extends Source {
7979
public: boolean;
8080
type: SourceType.Squad;
8181
members?: Connection<SourceMember>;
82+
topMembers?: UserShortProfile[];
8283
membersCount: number;
8384
description: string;
8485
memberPostingRole: SourceMemberRole;

0 commit comments

Comments
 (0)