Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1d278d4
feat(share): add a reusable copy link button
tomeredlich Sep 2, 2026
f218b9c
feat(sources): offer a copy link on the topic and directory surfaces
tomeredlich Sep 2, 2026
0132963
docs(snapshot): document the topic and directory page share placements
tomeredlich Sep 2, 2026
51cd869
feat(share): confirm a copy with the upvote button's arrow
tomeredlich Sep 3, 2026
bb3afa3
feat(squads): offer a copy link on the squad directory and squad page
tomeredlich Sep 3, 2026
7a40095
fix(share): adopt the cross-fade copy confirmation from #6570
tomeredlich Sep 3, 2026
ecbcb93
fix(squads): move the featured card copy link into the join row
tomeredlich Sep 3, 2026
7fad558
fix(squads): keep the featured card copy link visible
tomeredlich Sep 3, 2026
d4b6ff9
chore(snapshot): match the story to the surfaces that ship
tomeredlich Sep 3, 2026
c906787
Merge remote-tracking branch 'origin/main' into claude/snapshot-surfa…
idoshamun Sep 10, 2026
270e494
fix(share): log the provider and placement of every copy link
idoshamun Sep 10, 2026
1af2430
fix(share): copy the link inside the click in useShareOrCopyLink
idoshamun Sep 10, 2026
f1f7543
fix(sources): hide the hover-revealed copy links only for a mouse
idoshamun Sep 10, 2026
f78915e
chore(storybook): drop the topic and directory page mockup
idoshamun Sep 10, 2026
24e8cac
chore(sources): import the share campaign from its source file
idoshamun Sep 10, 2026
73234cb
Merge remote-tracking branch 'origin/main' into qa-6566
idoshamun Sep 10, 2026
afcc29b
fix(squads): offer the copy link on the squad directory list rows
idoshamun Sep 10, 2026
04e1400
fix(squads): show one link control in the squad header
idoshamun Sep 10, 2026
db5869b
fix(archive): offer the copy link on the best-of month and year pages
idoshamun Sep 10, 2026
b69b9ba
Merge remote-tracking branch 'origin/main' into qa-6566
idoshamun Sep 10, 2026
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
54 changes: 54 additions & 0 deletions packages/shared/src/components/archive/ArchiveCopyLinkButton.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 { ArchiveScopeType } from '../../graphql/archive';
import type { ArchiveScopeInfo } from '../../lib/archive';
import { CopyLinkButton } from '../share/CopyLinkButton';
import { LogEvent, Origin } from '../../lib/log';
import { ReferralCampaignKey } from '../../lib/referral';

interface ArchiveCopyLinkButtonProps {
scopeType: ArchiveScopeInfo['scopeType'];
scopeId?: string;
text: string;
}

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 const ArchiveCopyLinkButton = ({
scopeType,
scopeId,
text,
}: ArchiveCopyLinkButtonProps): ReactElement => {
const { event, cid } = shareByScope[scopeType];

return (
<CopyLinkButton
origin={Origin.ArchiveIndex}
shareProps={{
text,
link: globalThis?.location?.href,
cid,
logObject: () => ({
event_name: event,
target_id: scopeId,
}),
}}
/>
);
};
43 changes: 43 additions & 0 deletions packages/shared/src/components/archive/ArchiveFeedPage.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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 { ArchiveFeedPage } from './ArchiveFeedPage';
import { ArchivePeriodType, ArchiveScopeType } from '../../graphql/archive';
import { LogEvent, Origin } from '../../lib/log';
import { ShareProvider } from '../../lib/share';

it('logs a copy link on a monthly best-of page as an archive share', () => {
const logEvent = jest.fn();
Object.assign(navigator, {
clipboard: { writeText: jest.fn().mockResolvedValue(undefined) },
});
render(
<TestBootProvider
client={new QueryClient()}
auth={{ user: loggedUser }}
log={{ logEvent }}
>
<ArchiveFeedPage
archive={null}
scopeType={ArchiveScopeType.Global}
scopeName="daily.dev"
periodType={ArchivePeriodType.Month}
year={2025}
month={8}
/>
</TestBootProvider>,
);

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

expect(logEvent).toHaveBeenCalledWith({
event_name: LogEvent.ShareArchive,
target_id: undefined,
extra: JSON.stringify({
provider: ShareProvider.CopyLink,
origin: Origin.ArchiveIndex,
}),
});
});
18 changes: 13 additions & 5 deletions packages/shared/src/components/archive/ArchiveFeedPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import classNames from 'classnames';
import type { Archive, ArchiveItem } from '../../graphql/archive';
import { ArchivePeriodType } from '../../graphql/archive';
import type { ArchiveScopeInfo } from '../../lib/archive';
import { getArchiveTitle, getArchiveIndexUrl } from '../../lib/archive';
import { getArchiveIndexUrl, getArchivePeriodLabel } from '../../lib/archive';
import { ArchiveNavigation } from './ArchiveNavigation';
import { ArchivePostItem } from './ArchivePostItem';
import { ElementPlaceholder } from '../ElementPlaceholder';
import Link from '../utilities/Link';
import { ArrowIcon } from '../icons';
import { IconSize } from '../Icon';
import { ArchiveCopyLinkButton } from './ArchiveCopyLinkButton';

interface ArchiveFeedPageProps {
scopeType: ArchiveScopeInfo['scopeType'];
Expand Down Expand Up @@ -83,7 +84,7 @@ export function ArchiveFeedPage({
isLoading,
className,
}: ArchiveFeedPageProps): ReactElement {
const title = getArchiveTitle({
const periodLabel = getArchivePeriodLabel({
periodType,
periodStart:
periodType === ArchivePeriodType.Month && month
Expand All @@ -104,9 +105,16 @@ export function ArchiveFeedPage({
)}
>
{/* Header */}
<h1 className="mx-4 font-bold typo-title2 tablet:typo-title1">
Best of {scopeName} &mdash; {title.replace('Best of ', '')}
</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; {periodLabel}
</h1>
<ArchiveCopyLinkButton
scopeType={scopeType}
scopeId={scopeId}
text={`Check out the best of ${scopeName} from ${periodLabel}`}
/>
</div>

{/* Top navigation */}
<ArchiveNavigation
Expand Down
14 changes: 11 additions & 3 deletions packages/shared/src/components/archive/ArchiveIndexPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Link from '../utilities/Link';
import { ArrowIcon } from '../icons';
import { IconSize } from '../Icon';
import { ElementPlaceholder } from '../ElementPlaceholder';
import { ArchiveCopyLinkButton } from './ArchiveCopyLinkButton';

interface ArchiveIndexPageProps {
scopeType: ArchiveScopeInfo['scopeType'];
Expand Down Expand Up @@ -163,9 +164,16 @@ export function ArchiveIndexPage({
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>
<ArchiveCopyLinkButton
scopeType={scopeType}
scopeId={scopeId}
text={`Check out the best of ${scopeName} on daily.dev`}
/>
</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
39 changes: 38 additions & 1 deletion packages/shared/src/components/cards/squad/SquadList.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { RenderResult } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
import nock from 'nock';
Expand All @@ -21,6 +21,9 @@ import {
CONTENT_PREFERENCE_STATUS_QUERY,
ContentPreferenceType,
} from '../../../graphql/contentPreference';
import { TestBootProvider } from '../../../../__tests__/helpers/boot';
import { LogEvent, Origin } from '../../../lib/log';
import { ShareProvider } from '../../../lib/share';

const squadsList = [generateTestSquad()];
const members = generateMembersList();
Expand Down Expand Up @@ -81,6 +84,40 @@ it('should render the component and member count when members are provided', ()
expect(memberCount.innerHTML).toEqual(`${length} members`);
});

it('copies the squad link from the row without following the row link', () => {
const writeText = jest.fn().mockResolvedValue(undefined);
const logEvent = jest.fn();
const onRowLinkClick = jest.fn();
Object.assign(navigator, { clipboard: { writeText } });
render(
<TestBootProvider
client={new QueryClient()}
auth={{ user: loggedUser }}
log={{ logEvent }}
>
<SquadList squad={admin.source} />
</TestBootProvider>,
);
screen
.getByTitle(admin.source.name)
.addEventListener('click', onRowLinkClick);

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

expect(writeText).toHaveBeenCalledWith(
`${admin.source.permalink}?userid=${loggedUser.id}&cid=share_source`,
);
expect(logEvent).toHaveBeenCalledWith({
event_name: LogEvent.ShareSource,
target_id: admin.source.id,
extra: JSON.stringify({
provider: ShareProvider.CopyLink,
origin: Origin.SquadDirectory,
}),
});
expect(onRowLinkClick).not.toHaveBeenCalled();
});

it('should render the component with a view squad button', async () => {
mockGraphQL({
request: {
Expand Down
Loading
Loading