Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions packages/shared/src/components/errors/SquadLoading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,45 @@ const Actions = ({ className }: HTMLAttributes<HTMLDivElement>) => (
</FlexRow>
);

const MemberCard = ({
className,
showBadge = true,
}: HTMLAttributes<HTMLDivElement> & { showBadge?: boolean }) => (
<FlexRow
className={classNames(
'items-center gap-2 rounded-10 border border-border-subtlest-tertiary p-2',
className,
)}
>
<PlaceholderElement className="h-10 w-10 rounded-full" />
<FlexCol className="gap-1">
<RectangleElement className="h-4 w-20" />
{showBadge && <RectangleElement className="h-4 w-14" />}
</FlexCol>
</FlexRow>
);

const MemberRow = ({
titleWidthClassName,
className,
showOverflow = false,
showBadge = true,
}: HTMLAttributes<HTMLDivElement> & {
titleWidthClassName: string;
showOverflow?: boolean;
showBadge?: boolean;
}) => (
<FlexCol className={classNames('w-full items-start', className)}>
<RectangleElement className={classNames('h-4', titleWidthClassName)} />
<FlexRow className="mt-2 w-full items-center gap-3 overflow-hidden">
<MemberCard showBadge={showBadge} />
<MemberCard showBadge={showBadge} />
<MemberCard showBadge={showBadge} />
{showOverflow && <RectangleElement className="h-12 w-12 rounded-12" />}
</FlexRow>
</FlexCol>
);

function SquadLoading({
squad,
sidebarRendered,
Expand Down Expand Up @@ -59,6 +98,19 @@ function SquadLoading({
<RectangleElement className="h-12 w-12 tablet:w-32" />
<RectangleElement className="hidden h-12 w-32 tablet:flex" />
</FlexRow>
<MemberRow
titleWidthClassName="mt-6 w-24"
className="max-w-[38.5rem]"
showOverflow
/>
{squad?.public && (
<MemberRow
titleWidthClassName="mt-4 w-24"
className="max-w-[38.5rem]"
showOverflow
showBadge={false}
/>
)}
<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" />
</FlexCol>
<ColumnContainer className="relative max-h-page w-full overflow-hidden px-16 pt-7">
Expand Down
7 changes: 7 additions & 0 deletions packages/shared/src/components/modals/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ const PrivilegedMemberModal = dynamic(
/* webpackChunkName: "privilegedMembersModal" */ './squads/PrivilegedMembersModal'
),
);
const TopMembersModal = dynamic(
() =>
import(
/* webpackChunkName: "topMembersModal" */ './squads/TopMembersModal'
),
);

const BookmarkReminderModal = dynamic(
() =>
Expand Down Expand Up @@ -481,6 +487,7 @@ export const modals = {
[LazyModal.MarketingCta]: MarketingCtaModal,
[LazyModal.Share]: ShareModal,
[LazyModal.PrivilegedMembers]: PrivilegedMemberModal,
[LazyModal.TopMembers]: TopMembersModal,
[LazyModal.BookmarkReminder]: BookmarkReminderModal,
[LazyModal.RecoverStreak]: StreakRecoverModal,
[LazyModal.SlackIntegration]: SlackIntegrationModal,
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/components/modals/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export enum LazyModal {
MarketingCta = 'marketingCta',
Share = 'share',
PrivilegedMembers = 'privilegedMembers',
TopMembers = 'topMembers',
BookmarkReminder = 'bookmarkReminder',
SlackIntegration = 'slackIntegration',
ReportSource = 'reportSource',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import type { ReactElement } from 'react';
import React from 'react';
import Link from '../../utilities/Link';
import type { ModalProps } from '../common/Modal';
import { Modal } from '../common/Modal';
import type { Source } from '../../../graphql/sources';
import { UserShortInfo } from '../../profile/UserShortInfo';
import { Origin } from '../../../lib/log';
import { useSquad } from '../../../hooks';

import { generateQueryKey, RequestKey } from '../../../lib/query';
import { useAuthContext } from '../../../contexts/AuthContext';
import { useSourceContentPreferenceMutationSubscription } from '../../../hooks/contentPreference/useSourceContentPreferenceMutationSubscription';
import { SquadUsersModal } from './SquadUsersModal';

export interface PrivilegedMembersModalProps
extends Omit<ModalProps, 'children'> {
Expand All @@ -21,30 +13,18 @@ function PrivilegedMembersModal({
source,
...props
}: PrivilegedMembersModalProps): ReactElement {
const { user: loggedUser } = useAuthContext();
const { squad } = useSquad({ handle: source.handle });

useSourceContentPreferenceMutationSubscription({
queryKey: generateQueryKey(RequestKey.Squad, loggedUser, source.handle),
});

return (
<Modal kind={Modal.Kind.FlexibleCenter} size={Modal.Size.Medium} {...props}>
<Modal.Header title="Moderated by" />
<Modal.Body className="!p-0">
{squad?.privilegedMembers?.map(({ user, role }) => (
<Link key={user.username} href={user.permalink}>
<UserShortInfo
tag="a"
href={user.permalink}
user={{ ...user, role }}
showFollow
origin={Origin.SquadMembersList}
/>
</Link>
))}
</Modal.Body>
</Modal>
<SquadUsersModal
{...props}
source={source}
title="Moderated by"
getUsers={(squad) =>
squad?.privilegedMembers?.map(({ user, role }) => ({
...user,
role,
})) ?? []
}
/>
);
}

Expand Down
54 changes: 54 additions & 0 deletions packages/shared/src/components/modals/squads/SquadUsersModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { ReactElement } from 'react';
import React from 'react';
import type { UserShortProfile } from '../../../lib/user';
import type { Source, SourceMemberRole, Squad } from '../../../graphql/sources';
import { UserShortInfo } from '../../profile/UserShortInfo';
import type { ModalProps } from '../common/Modal';
import { Modal } from '../common/Modal';
import { Origin } from '../../../lib/log';
import { useSquad } from '../../../hooks';
import { useAuthContext } from '../../../contexts/AuthContext';
import { useSourceContentPreferenceMutationSubscription } from '../../../hooks/contentPreference/useSourceContentPreferenceMutationSubscription';
import { generateQueryKey, RequestKey } from '../../../lib/query';

export interface SquadUsersModalProps extends Omit<ModalProps, 'children'> {
source: Pick<Source, 'handle'>;
title: string;
getUsers: (
squad: Squad | undefined,
) => Array<UserShortProfile & { role?: SourceMemberRole }>;
}

export function SquadUsersModal({
source,
title,
getUsers,
...props
}: SquadUsersModalProps): ReactElement {
const { user: loggedUser } = useAuthContext();
const { squad } = useSquad({ handle: source.handle });

useSourceContentPreferenceMutationSubscription({
queryKey: generateQueryKey(RequestKey.Squad, loggedUser, source.handle),
});

const users = getUsers(squad);

return (
<Modal kind={Modal.Kind.FlexibleCenter} size={Modal.Size.Medium} {...props}>
<Modal.Header title={title} />
<Modal.Body className="!p-0">
{users.map((user) => (
<UserShortInfo
key={user.id}
tag="a"
href={user.permalink}
user={user}
showFollow
origin={Origin.SquadMembersList}
/>
))}
</Modal.Body>
</Modal>
);
}
25 changes: 25 additions & 0 deletions packages/shared/src/components/modals/squads/TopMembersModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { ReactElement } from 'react';
import React from 'react';
import type { ModalProps } from '../common/Modal';
import type { Source } from '../../../graphql/sources';
import { SquadUsersModal } from './SquadUsersModal';

export interface TopMembersModalProps extends Omit<ModalProps, 'children'> {
source: Pick<Source, 'handle'>;
}

function TopMembersModal({
source,
...props
}: TopMembersModalProps): ReactElement {
return (
<SquadUsersModal
{...props}
source={source}
title="Top members"
getUsers={(squad) => squad?.topMembers ?? []}
/>
);
}

export default TopMembersModal;
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import type { ReactElement } from 'react';
import React from 'react';
import { ProfileImageSize, ProfilePicture } from '../../ProfilePicture';
import type { SourceMember } from '../../../graphql/sources';
import { ProfileTooltip } from '../../profile/ProfileTooltip';
import type { SourceMember, SourceMemberRole } from '../../../graphql/sources';
import { SourceMemberRole as SourceMemberRoleEnum } from '../../../graphql/sources';
import { ProfileLink } from '../../profile/ProfileLink';
import { ProfileTooltip } from '../../profile/ProfileTooltip';
import UserBadge from '../../UserBadge';
import { getRoleName } from '../../utilities';

interface PrivilegedMemberItemProps {
member: SourceMember;
user: SourceMember['user'];
badge?: string;
role?: SourceMemberRole;
}

export function PrivilegedMemberItem({
member: { user, role },
user,
badge,
role = SourceMemberRoleEnum.Member,
}: PrivilegedMemberItemProps): ReactElement {
return (
<ProfileTooltip userId={user.id} tooltip={{ placement: 'bottom' }}>
Expand All @@ -25,9 +29,11 @@ export function PrivilegedMemberItem({
<span className="flex truncate text-text-tertiary typo-subhead">
{user.name}
</span>
<UserBadge className="w-fit" role={role}>
{getRoleName(role)}
</UserBadge>
{badge && (
<UserBadge className="w-fit" role={role}>
{badge}
</UserBadge>
)}
</div>
</ProfileLink>
</ProfileTooltip>
Expand Down
49 changes: 46 additions & 3 deletions packages/shared/src/components/squads/SquadPageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import type { ReactElement } from 'react';
import React from 'react';
import classNames from 'classnames';
import type { BasicSourceMember, Squad } from '../../graphql/sources';
import { SourcePermissions } from '../../graphql/sources';
import { SourceMemberRole, SourcePermissions } from '../../graphql/sources';
import { SquadHeaderBar } from './SquadHeaderBar';
import { SquadImage } from './SquadImage';
import { FlexCentered, FlexCol } from '../utilities';
import { FlexCentered, FlexCol, getRoleName } from '../utilities';
import SharePostBar from './SharePostBar';
import { verifyPermission } from '../../graphql/squads';
import { Button, ButtonColor, ButtonVariant } from '../buttons/Button';
Expand Down Expand Up @@ -57,6 +57,8 @@ export function SquadPageHeader({
? formatMonthYearOnly(squad.createdAt)
: null;
const privilegedLength = squad.privilegedMembers?.length || 0;
const topMembers = squad.topMembers ?? [];
const topMembersLength = topMembers.length;
const isMobile = useViewSize(ViewSize.MobileL);
const listMax = isMobile
? MAX_VISIBLE_PRIVILEGED_MEMBERS_MOBILE
Expand Down Expand Up @@ -164,7 +166,12 @@ export function SquadPageHeader({
</Typography>
<div className="mt-2 flex flex-row items-center gap-3">
{squad.privilegedMembers?.slice(0, listMax).map((member) => (
<PrivilegedMemberItem key={member.user.id} member={member} />
<PrivilegedMemberItem
key={member.user.id}
user={member.user}
role={member.role}
badge={getRoleName(member.role)}
/>
))}
{privilegedLength > listMax && (
<Button
Expand All @@ -181,6 +188,42 @@ export function SquadPageHeader({
</Button>
)}
</div>
{topMembers.length > 0 && (
<>
<Typography
bold
className="mt-4"
color={TypographyColor.Tertiary}
tag={TypographyTag.Span}
type={TypographyType.Caption1}
>
Top members
</Typography>
<div className="mt-2 flex flex-row items-center gap-3">
{topMembers.slice(0, listMax).map((member) => (
<PrivilegedMemberItem
key={member.id}
user={member}
role={SourceMemberRole.Member}
/>
))}
{topMembersLength > listMax && (
<Button
variant={ButtonVariant.Tertiary}
className="aspect-square border border-border-subtlest-tertiary"
onClick={() =>
openModal({
type: LazyModal.TopMembers,
props: { source: squad },
})
}
>
+{topMembersLength - listMax}
</Button>
)}
</div>
</>
)}
<div className={classNames('w-full', MAX_WIDTH)}>
<SquadStack squad={squad} />
</div>
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/graphql/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export interface Squad extends Source {
public: boolean;
type: SourceType.Squad;
members?: Connection<SourceMember>;
topMembers?: UserShortProfile[];
membersCount: number;
description: string;
memberPostingRole: SourceMemberRole;
Expand Down
Loading
Loading