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
43 changes: 40 additions & 3 deletions packages/shared/src/components/archive/ArchiveIndexPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ReactElement } from 'react';
import React from 'react';
import classNames from 'classnames';
import type { Archive } from '../../graphql/archive';
import { ArchiveScopeType } from '../../graphql/archive';
import type { ArchiveScopeInfo, ArchivesByYear } from '../../lib/archive';
import {
getArchiveUrlFromArchive,
Expand All @@ -13,6 +14,9 @@ import Link from '../utilities/Link';
import { ArrowIcon } from '../icons';
import { IconSize } from '../Icon';
import { ElementPlaceholder } from '../ElementPlaceholder';
import { CopyLinkButton } from '../share/CopyLinkButton';
import { LogEvent, Origin } from '../../lib/log';
import { ReferralCampaignKey } from '../../lib/referral';

interface ArchiveIndexPageProps {
scopeType: ArchiveScopeInfo['scopeType'];
Expand Down Expand Up @@ -150,6 +154,24 @@ function ArchiveGrid({
);
}

const shareByScope: Record<
ArchiveScopeInfo['scopeType'],
{ event: LogEvent; cid: ReferralCampaignKey }
> = {
[ArchiveScopeType.Global]: {
event: LogEvent.ShareArchive,
cid: ReferralCampaignKey.Generic,
},
[ArchiveScopeType.Tag]: {
event: LogEvent.ShareTag,
cid: ReferralCampaignKey.ShareTag,
},
[ArchiveScopeType.Source]: {
event: LogEvent.ShareSource,
cid: ReferralCampaignKey.ShareSource,
},
};

export function ArchiveIndexPage({
archives,
scopeType,
Expand All @@ -159,13 +181,28 @@ export function ArchiveIndexPage({
className,
}: ArchiveIndexPageProps): ReactElement {
const groups = groupArchivesByYear(archives);
const { event: shareEvent, cid: shareCampaign } = shareByScope[scopeType];

return (
<div className={classNames('flex flex-col', className)}>
{/* Header */}
<h1 className="mx-4 font-bold typo-title2 tablet:typo-title1">
Best of {scopeName} &mdash; Archive
</h1>
<div className="mx-4 flex items-center gap-2">
<h1 className="flex-1 font-bold typo-title2 tablet:typo-title1">
Best of {scopeName} &mdash; Archive
</h1>
<CopyLinkButton
origin={Origin.ArchiveIndex}
shareProps={{
text: `Check out the best of ${scopeName} on daily.dev`,
link: globalThis?.location?.href,
cid: shareCampaign,
logObject: () => ({
event_name: shareEvent,
target_id: scopeId,
}),
}}
/>
</div>

{/* Archive grid by year */}
<ArchiveGrid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import type { CommonLeaderboardProps } from './LeaderboardList';
import { LeaderboardList } from './LeaderboardList';
import { LeaderboardListItem } from './LeaderboardListItem';
import { UserHighlight, UserType } from '../../widgets/PostUsersHighlights';
import { CopyLinkButton } from '../../share/CopyLinkButton';
import { ButtonVariant } from '../../buttons/Button';
import { ReferralCampaignKey } from '../../../lib/referral';
import { LogEvent, Origin } from '../../../lib/log';

export function SourceTopList({
items,
Expand All @@ -16,7 +20,7 @@ export function SourceTopList({
<LeaderboardListItem
key={item.id}
index={i + 1}
className="flex w-full flex-row items-center rounded-8 px-2 hover:bg-accent-pepper-subtler"
className="group/source flex w-full flex-row items-center rounded-8 px-2 hover:bg-accent-pepper-subtler"
>
<UserHighlight
{...item}
Expand All @@ -30,6 +34,21 @@ export function SourceTopList({
}}
allowSubscribe={false}
/>
{/* Hover-revealed only where hover exists; always there on touch. */}
<CopyLinkButton
variant={ButtonVariant.Tertiary}
className="ml-auto shrink-0 laptop:mouse:opacity-0 laptop:mouse:group-focus-within/source:opacity-100 laptop:mouse:group-hover/source:opacity-100"
origin={Origin.SourceDirectory}
shareProps={{
text: `Check out ${item.handle} on daily.dev`,
link: item.permalink,
cid: ReferralCampaignKey.ShareSource,
logObject: () => ({
event_name: LogEvent.ShareSource,
target_id: item.id,
}),
}}
/>
</LeaderboardListItem>
))}
</LeaderboardList>
Expand Down
40 changes: 31 additions & 9 deletions packages/shared/src/components/cards/squad/SquadGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { Image, ImageType } from '../../image/Image';
import { cloudinarySquadsDirectoryCardBannerDefault } from '../../../lib/image';
import type { UnFeaturedSquadCardProps } from './common/types';
import { SquadActionButton } from '../../squads/SquadActionButton';
import { Origin } from '../../../lib/log';
import { ButtonVariant } from '../../buttons/common';
import { LogEvent, Origin } from '../../../lib/log';
import { ButtonSize, ButtonVariant } from '../../buttons/common';
import { anchorDefaultRel } from '../../../lib/strings';
import { useCampaignById } from '../../../graphql/campaigns';
import { Tooltip } from '../../tooltip/Tooltip';
Expand All @@ -27,6 +27,8 @@ import {
import { useSquadsDirectoryLogging } from './common/useSquadsDirectoryLogging';
import { AdViewability } from '../ad/common/AdViewability';
import { useScrambler } from '../../../hooks/useScrambler';
import { CopyLinkButton } from '../../share/CopyLinkButton';
import { ReferralCampaignKey } from '../../../lib/referral';

export enum SourceCardBorderColor {
Avocado = 'avocado',
Expand Down Expand Up @@ -87,6 +89,15 @@ export const SquadGrid = ({
});
const borderColor = border || color || SourceCardBorderColor.Avocado;
const { ref, onClickAd, onViewableAd } = useSquadsDirectoryLogging(ad);
const shareProps = {
text: `Check out the ${name} squad on daily.dev`,
link: permalink,
cid: ReferralCampaignKey.ShareSource,
logObject: () => ({
event_name: LogEvent.ShareSource,
target_id: source.id,
}),
};
const promotedText = useScrambler('Promoted');
const promotedByTooltip = useScrambler(
campaign ? `Promoted by @${campaign.user.username}` : null,
Expand Down Expand Up @@ -158,13 +169,24 @@ export const SquadGrid = ({
)}
</div>

<SquadActionButton
className={{ button: 'z-0 w-full' }}
squad={source}
origin={Origin.SquadDirectory}
data-testid="squad-action"
buttonVariants={[ButtonVariant.Secondary, ButtonVariant.Float]}
/>
<div className="flex items-center gap-2">
<div className="flex-1">
<SquadActionButton
className={{ button: 'z-0 w-full' }}
squad={source}
origin={Origin.SquadDirectory}
data-testid="squad-action"
buttonVariants={[ButtonVariant.Secondary, ButtonVariant.Float]}
/>
</div>
<CopyLinkButton
className="relative z-0 shrink-0"
origin={Origin.SquadDirectory}
shareProps={shareProps}
size={ButtonSize.Medium}
variant={ButtonVariant.Tertiary}
/>
</div>
</div>
</div>
{children}
Expand Down
47 changes: 36 additions & 11 deletions packages/shared/src/components/cards/squad/UnfeaturedSquadGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,38 @@ import {
TypographyTag,
TypographyType,
} from '../../typography/Typography';
import { largeNumberFormat } from '../../../lib';
import { largeNumberFormat } from '../../../lib/numberFormat';
import { Separator } from '../common/common';
import type { UnFeaturedSquadCardProps } from './common/types';
import { Origin } from '../../../lib/log';
import { LogEvent, Origin } from '../../../lib/log';
import { SquadActionButton } from '../../squads/SquadActionButton';
import { ButtonVariant } from '../../buttons/common';
import { ButtonSize, ButtonVariant } from '../../buttons/common';
import { Image, ImageType } from '../../image/Image';
import { CopyLinkButton } from '../../share/CopyLinkButton';
import { ReferralCampaignKey } from '../../../lib/referral';

export const UnfeaturedSquadGrid = ({
source,
className,
}: UnFeaturedSquadCardProps): ReactElement => {
const title = source.name;
const shareProps = {
text: `Check out the ${title} squad on daily.dev`,
link: source.permalink,
cid: ReferralCampaignKey.ShareSource,
logObject: () => ({
event_name: LogEvent.ShareSource,
target_id: source.id,
}),
};

return (
<Card className={classNames('overflow-hidden border-0 p-4', className)}>
<Card
className={classNames(
'group/squad overflow-hidden border-0 p-4',
className,
)}
>
<CardLink
href={source.permalink}
rel="noopener"
Expand All @@ -36,13 +52,22 @@ export const UnfeaturedSquadGrid = ({
className="size-16 rounded-full"
type={ImageType.Squad}
/>
<SquadActionButton
className={{ button: 'z-0' }}
squad={source}
origin={Origin.SquadDirectory}
data-testid="squad-action"
buttonVariants={[ButtonVariant.Secondary, ButtonVariant.Float]}
/>
<div className="flex items-center gap-2">
<CopyLinkButton
className="relative z-0 laptop:mouse:opacity-0 laptop:mouse:group-focus-within/squad:opacity-100 laptop:mouse:group-hover/squad:opacity-100"
origin={Origin.SquadDirectory}
shareProps={shareProps}
size={ButtonSize.Medium}
variant={ButtonVariant.Tertiary}
/>
<SquadActionButton
className={{ button: 'z-0' }}
squad={source}
origin={Origin.SquadDirectory}
data-testid="squad-action"
buttonVariants={[ButtonVariant.Secondary, ButtonVariant.Float]}
/>
</div>
</div>
<Typography
tag={TypographyTag.H1}
Expand Down
65 changes: 65 additions & 0 deletions packages/shared/src/components/share/CopyLinkButton.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from 'react';
import { QueryClient } from '@tanstack/react-query';
import { fireEvent, render, screen } from '@testing-library/react';
import { TestBootProvider } from '../../../__tests__/helpers/boot';
import loggedUser from '../../../__tests__/fixture/loggedUser';
import { CopyLinkButton } from './CopyLinkButton';
import { LogEvent, Origin } from '../../lib/log';
import { ReferralCampaignKey } from '../../lib/referral';
import { ShareProvider } from '../../lib/share';

const writeText = jest.fn().mockResolvedValue(undefined);
const logEvent = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
Object.assign(navigator, { clipboard: { writeText } });
});

const renderComponent = () =>
render(
<TestBootProvider
client={new QueryClient()}
auth={{ user: loggedUser }}
log={{ logEvent }}
>
<CopyLinkButton
origin={Origin.SourcePage}
shareProps={{
text: 'Check out theverge on daily.dev',
link: 'https://app.daily.dev/sources/theverge',
cid: ReferralCampaignKey.ShareSource,
logObject: () => ({
event_name: LogEvent.ShareSource,
target_id: 'theverge',
}),
}}
/>
</TestBootProvider>,
);

it('writes the tracked link within the click, before the shortener answers', () => {
renderComponent();

// No await: Safari refuses a clipboard write once the click task has ended.
fireEvent.click(screen.getByRole('button', { name: 'Copy link' }));

expect(writeText).toHaveBeenCalledWith(
'https://app.daily.dev/sources/theverge?userid=u1&cid=share_source',
);
});

it('logs the share with its provider and placement', () => {
renderComponent();

fireEvent.click(screen.getByRole('button', { name: 'Copy link' }));

expect(logEvent).toHaveBeenCalledWith({
event_name: LogEvent.ShareSource,
target_id: 'theverge',
extra: JSON.stringify({
provider: ShareProvider.CopyLink,
origin: Origin.SourcePage,
}),
});
});
52 changes: 52 additions & 0 deletions packages/shared/src/components/share/CopyLinkButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { ReactElement } from 'react';
import React from 'react';
import { Button, ButtonSize, ButtonVariant } from '../buttons/Button';
import { LinkIcon } from '../icons/Link';
import { CopyStateIcon } from './CopyStateIcon';
import { Tooltip } from '../tooltip/Tooltip';
import type { UseShareOrCopyLinkProps } from '../../hooks/useShareOrCopyLink';
import { useShareOrCopyLink } from '../../hooks/useShareOrCopyLink';
import type { Origin } from '../../lib/log';

interface CopyLinkButtonProps {
shareProps: UseShareOrCopyLinkProps &
Required<Pick<UseShareOrCopyLinkProps, 'logObject'>>;
origin: Origin;
className?: string;
size?: ButtonSize;
variant?: ButtonVariant;
}

const LABEL = 'Copy link';
const COPIED_LABEL = 'Copied!';

export const CopyLinkButton = ({
shareProps,
origin,
className,
size = ButtonSize.Small,
variant = ButtonVariant.Float,
}: CopyLinkButtonProps): ReactElement => {
// Stays false on the native-share path, where the OS sheet is the feedback.
const [copied, onShareOrCopyLink] = useShareOrCopyLink({
...shareProps,
logObject: (provider) => ({
...shareProps.logObject(provider),
extra: JSON.stringify({ provider, origin }),
}),
});

return (
<Tooltip content={copied ? COPIED_LABEL : LABEL}>
<Button
aria-label={LABEL}
className={className}
icon={<CopyStateIcon copied={copied} icon={LinkIcon} />}
onClick={() => onShareOrCopyLink()}
size={size}
type="button"
variant={variant}
/>
</Tooltip>
);
};
Loading
Loading