Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const getReadPostButtonIcon = (
isPostOrSharedPostTwitter(post) ? (
<TwitterIcon size={IconSize.Size16} />
) : (
<OpenLinkIcon secondary />
<OpenLinkIcon />
);

type ReadArticleButtonProps = ButtonProps<'a'> & {
Expand All @@ -31,7 +31,7 @@ export const ReadArticleButton = ({
<Button
tag="a"
size={size}
icon={<OpenLinkIcon secondary />}
icon={<OpenLinkIcon />}
iconPosition={ButtonIconPosition.Right}
target={openNewTab ? '_blank' : '_self'}
{...props}
Expand Down
206 changes: 77 additions & 129 deletions packages/shared/src/components/post/tags/PostTagList.tsx
Original file line number Diff line number Diff line change
@@ -1,177 +1,125 @@
import type { PropsWithChildren, ReactElement } from 'react';
import type { ReactElement } from 'react';
import React, { memo, useMemo } from 'react';
import classNames from 'classnames';
import type { Post } from '../../../graphql/posts';
import { useFollowPostTags } from '../../../hooks/feed/useFollowPostTags';
import type { TypographyProps } from '../../typography/Typography';
import {
Typography,
TypographyColor,
TypographyTag,
TypographyType,
} from '../../typography/Typography';
import { getTagPageLink } from '../../../lib';
import { TagChip } from '../../tags/TagChip';
import { BrandedTag } from '../../brand/BrandedTag';
import { useBrandSponsorship } from '../../../hooks/useBrandSponsorship';
import Link from '../../utilities/Link';
import { getTagPageLink } from '../../../lib';
import { Button, ButtonSize } from '../../buttons/Button';
import { PlusIcon } from '../../icons';
import { IconSize } from '../../Icon';
import { Tooltip } from '../../tooltip/Tooltip';
import { BrandedTag } from '../../brand/BrandedTag';
import { useBrandSponsorship } from '../../../hooks/useBrandSponsorship';

interface PostTagListProps {
post: Post;
}

interface PostTagItemProps {
isFollowed?: boolean;
onFollow?: (tag: string) => void;
interface BrandedTagChipProps {
tag: string;
/** Render the BrandedTag wrapper for engagement-ads styling. Off by default — preserves the original chip layout when no creative is present. */
useBrandedRenderer?: boolean;
isFollowed: boolean;
onFollow: (tag: string) => void;
disableBranding?: boolean;
}

const Chip = <T extends TypographyTag>({
children,
className,
...props
}: PropsWithChildren<TypographyProps<T>>) => (
<Typography
className={classNames(
'cursor-pointer rounded-10 border-border-subtlest-tertiary transition-colors',
className,
)}
color={TypographyColor.Tertiary}
tag={TypographyTag.Span}
type={TypographyType.Caption1}
{...props}
>
{children}
</Typography>
);

const PostTagItem = ({
/**
* Engagement-ads variant — kept separate from the standard `TagChip`
* so the `BrandedTag` wrapper (with its own padding / border quirks)
* does not bleed into the unified chip design used everywhere else.
*/
const BrandedTagChip = ({
tag,
isFollowed,
onFollow,
tag,
useBrandedRenderer,
disableBranding,
}: PostTagItemProps): ReactElement => {
// Default rendering — matches the pre-engagement-ads layout exactly.
// Used when no creative is sponsoring any tag in the list.
if (!useBrandedRenderer) {
if (isFollowed) {
return (
<Link href={getTagPageLink(tag)} passHref prefetch={false}>
<Chip
className="border px-2 py-1 hover:bg-border-subtlest-tertiary"
role="listitem"
tag={TypographyTag.Link}
title={`Check all #${tag} posts`}
>
#{tag}
</Chip>
</Link>
);
}

return (
<Chip className="flex items-center bg-surface-float" role="listitem">
<Link href={getTagPageLink(tag)} passHref>
<a
className="inline-block px-2 py-1"
title={`Check all #${tag} posts`}
>{`#${tag}`}</a>
</Link>
}: BrandedTagChipProps): ReactElement => (
<span
role="listitem"
className={classNames(
'inline-flex items-center rounded-8',
isFollowed ? '' : 'bg-surface-float',
)}
>
<Link href={getTagPageLink(tag)} passHref prefetch={false}>
<a title={`Check all #${tag} posts`}>
<BrandedTag
tag={tag}
asSpan
disableBranding={disableBranding}
className={
isFollowed
? 'border px-2 py-1 hover:bg-border-subtlest-tertiary'
: '!h-auto !border-0 !bg-transparent py-1'
}
/>
</a>
</Link>
{!isFollowed && (
<>
<span
className="h-3 translate-y-px rounded-2 border border-border-subtlest-tertiary"
aria-hidden
role="separator"
className="mx-2 h-3 w-px bg-border-subtlest-tertiary"
/>
<Tooltip content={`Follow #${tag}`}>
<Button
type="button"
size={ButtonSize.XSmall}
icon={<PlusIcon aria-hidden size={IconSize.XSmall} />}
onClick={() => onFollow(tag)}
size={ButtonSize.XSmall}
type="button"
aria-label={`Follow #${tag}`}
/>
</Tooltip>
</Chip>
);
}

// Engagement-ads-aware rendering. Only used when at least one tag in
// the list is sponsored.
return (
<Chip
className={classNames(
'flex items-center',
isFollowed ? '' : 'bg-surface-float',
)}
role="listitem"
>
<Link href={getTagPageLink(tag)} passHref prefetch={false}>
<a title={`Check all #${tag} posts`}>
<BrandedTag
tag={tag}
asSpan
disableBranding={disableBranding}
className={
isFollowed
? 'border px-2 py-1 hover:bg-border-subtlest-tertiary'
: '!h-auto !border-0 !bg-transparent py-1'
}
/>
</a>
</Link>
{!isFollowed && (
<>
<span
className="h-3 translate-y-px rounded-2 border border-border-subtlest-tertiary"
role="separator"
/>
<Tooltip content={`Follow #${tag}`}>
<Button
icon={<PlusIcon aria-hidden size={IconSize.XSmall} />}
onClick={() => onFollow(tag)}
size={ButtonSize.XSmall}
type="button"
/>
</Tooltip>
</>
)}
</Chip>
);
};
</>
)}
</span>
);

const PostTagListInner = ({ post }: PostTagListProps): ReactElement => {
const { onFollowTag, tags } = useFollowPostTags({ post });
const { isTagSponsored } = useBrandSponsorship();

const firstSponsoredTag = useMemo(() => {
return tags.all.find((tag) => isTagSponsored(tag)) || null;
}, [tags.all, isTagSponsored]);
const firstSponsoredTag = useMemo(
() => tags.all.find((tag) => isTagSponsored(tag)) || null,
[tags.all, isTagSponsored],
);

if (!tags.all.length) {
return null;
}

const followedSet = new Set(tags.followed);

const useBrandedRenderer = firstSponsoredTag !== null;

return (
<ul aria-label="Post tags" className="flex flex-wrap items-center gap-2">
{tags.all.map((tag) => (
<PostTagItem
key={tag}
tag={tag}
isFollowed={followedSet.has(tag)}
onFollow={onFollowTag}
useBrandedRenderer={useBrandedRenderer}
disableBranding={tag !== firstSponsoredTag}
/>
))}
{tags.all.map((tag) => {
const isFollowed = followedSet.has(tag);

if (useBrandedRenderer) {
return (
<BrandedTagChip
key={tag}
tag={tag}
isFollowed={isFollowed}
onFollow={onFollowTag}
disableBranding={tag !== firstSponsoredTag}
/>
);
}

return (
<TagChip
key={tag}
tag={tag}
size="sm"
isFollowed={isFollowed}
onFollow={onFollowTag}
/>
);
})}
</ul>
);
};
Expand Down
6 changes: 3 additions & 3 deletions packages/shared/src/components/profile/ProfileHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const ProfileHeader = ({
)}
tag="a"
disabled={!isSameUser}
type={ButtonVariant.Float}
variant={ButtonVariant.Float}
icon={<EditIcon />}
aria-label="Edit profile"
/>
Expand All @@ -99,7 +99,7 @@ const ProfileHeader = ({
<div className="flex flex-col gap-2">
{bio && <Typography type={TypographyType.Body}>{bio}</Typography>}
<div className="flex items-center">
{user?.companies?.length > 0 && (
{!!user?.companies?.length && (
<VerifiedCompanyUserBadge
size={ProfileImageSize.XSmall}
user={user}
Expand All @@ -110,7 +110,7 @@ const ProfileHeader = ({
}}
/>
)}
{user?.companies?.length > 0 && user?.location && (
{!!user?.companies?.length && user?.location && (
<Separator className="text-text-secondary" />
)}
{user?.location && (
Expand Down
9 changes: 8 additions & 1 deletion packages/shared/src/components/sidebar/SidebarMenuIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from 'react';
import classNames from 'classnames';
import { Button, ButtonSize, ButtonVariant } from '../buttons/Button';
import { SidebarArrowLeft, SidebarArrowRight } from '../icons';
import { IconSize } from '../Icon';
import { useSettingsContext } from '../../contexts/SettingsContext';
import { useLogContext } from '../../contexts/LogContext';
import { Tooltip } from '../tooltip/Tooltip';
Expand Down Expand Up @@ -40,7 +41,13 @@ export const SidebarMenuIcon = (): ReactElement => {
size={ButtonSize.XSmall}
onClick={logAndToggleSidebarExpanded}
className="text-text-tertiary hover:text-text-primary"
icon={sidebarExpanded ? <SidebarArrowLeft /> : <SidebarArrowRight />}
icon={
sidebarExpanded ? (
<SidebarArrowLeft size={IconSize.Size16} />
) : (
<SidebarArrowRight size={IconSize.Size16} />
)
}
aria-label={sidebarExpanded ? 'Close sidebar' : 'Open sidebar'}
/>
</Tooltip>
Expand Down
17 changes: 13 additions & 4 deletions packages/shared/src/components/tags/TagChip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,11 @@ const TagLabel = ({
tag={TypographyTag.Link}
type={type}
color={TypographyColor.Tertiary}
className="cursor-pointer no-underline transition-colors hover:text-text-primary"
// `min-w-0 truncate` keeps very long tags (e.g. polyglot
// multi-word system labels) inside the chip instead of pushing
// the trailing `+` button off-screen. The chip itself can be
// capped by callers via `className="max-w-..."`.
className="block min-w-0 cursor-pointer truncate no-underline transition-colors hover:text-text-primary"
title={`Check all #${tag} posts`}
>
#{tag}
Expand All @@ -123,7 +127,9 @@ export const TagChip = ({
<span
role="listitem"
className={classNames(
'inline-flex items-center border border-border-subtlest-tertiary transition-colors hover:bg-border-subtlest-tertiary',
// `max-w-full` keeps the chip from breaking out of its
// column / flex parent on extremely long tags.
'inline-flex max-w-full items-center border border-border-subtlest-tertiary transition-colors hover:bg-border-subtlest-tertiary',
cfg.height,
cfg.radius,
cfg.horizontalPlain,
Expand All @@ -141,7 +147,7 @@ export const TagChip = ({
<span
role="listitem"
className={classNames(
'inline-flex items-center bg-surface-float',
'inline-flex max-w-full items-center bg-surface-float',
cfg.height,
cfg.radius,
showAction ? cfg.horizontalAction : cfg.horizontalPlain,
Expand All @@ -155,14 +161,17 @@ export const TagChip = ({
role="separator"
aria-hidden
className={classNames(
'mx-2 w-px bg-border-subtlest-tertiary',
// `shrink-0` so the separator never collapses when the
// label needs to truncate.
'mx-2 w-px shrink-0 bg-border-subtlest-tertiary',
cfg.separatorHeight,
)}
/>
<Tooltip content={`Follow #${tag}`}>
<ButtonV2
type="button"
size={cfg.button}
className="shrink-0"
icon={<PlusIcon aria-hidden size={cfg.icon} />}
onClick={() => onFollow?.(tag)}
aria-label={`Follow #${tag}`}
Expand Down
Loading
Loading