Skip to content

Commit 7b12d0c

Browse files
tsahimatsliahclaude
andcommitted
feat(explore): signup-first auth hero above the tag page
Replace the simple signup banner with an inline, compact version of the new-tab hijacking strip (#6127): a "Where developers make every tab count." auth hero with the onboarding background and the shared AuthOptions onboarding-signup social options (Google / GitHub / email), placed above everything on the tag page so logged-out SEO/AEO visitors are prompted to sign up first. Renders only for logged-out users; keeps impression/click analytics. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2646f08 commit 7b12d0c

4 files changed

Lines changed: 167 additions & 135 deletions

File tree

packages/shared/src/components/explore/ExploreTopicPage.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ import UserEntityCard from '../cards/entity/UserEntityCard';
5555
import SourceEntityCard from '../cards/entity/SourceEntityCard';
5656
import EntityCardSkeleton from '../cards/entity/EntityCardSkeleton';
5757
import { ExploreHeader } from './ExploreHeader';
58-
import { TagSignupBanner } from './TagSignupBanner';
58+
import { TagSignupHero } from './TagSignupHero';
5959
import { largeNumberFormat } from '../../lib';
6060
import { webappUrl } from '../../lib/constants';
6161
import {
@@ -340,6 +340,9 @@ export const ExploreTopicPage = ({
340340
.filter((name): name is string => !!name)}
341341
/>
342342
<div className="flex w-full flex-col px-4 py-6 tablet:px-6">
343+
{/* Signup-first hero above everything for logged-out visitors. */}
344+
<TagSignupHero tag={title} className="mb-8" />
345+
343346
{/* Hero cover — centered on the page; content below spans full width. */}
344347
<header className="mx-auto flex w-full max-w-[48rem] flex-col items-center gap-4 py-8 text-center">
345348
<SponsoredTagHero tag={tag} />
@@ -486,9 +489,6 @@ export const ExploreTopicPage = ({
486489

487490
<div className="mb-2 h-px w-full bg-border-subtlest-tertiary" />
488491

489-
{/* Convert logged-out SEO/AEO visitors — sits above the feed. */}
490-
<TagSignupBanner tag={title} className="mb-10" />
491-
492492
{/* Recommended stories */}
493493
<ActiveFeedNameContext.Provider
494494
value={{ feedName: OtherFeedPage.TagsTopPosts }}

packages/shared/src/components/explore/TagSignupBanner.tsx

Lines changed: 0 additions & 111 deletions
This file was deleted.

packages/shared/src/components/explore/TagSignupBanner.spec.tsx renamed to packages/shared/src/components/explore/TagSignupHero.spec.tsx

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import React from 'react';
2-
import { fireEvent, render, screen } from '@testing-library/react';
2+
import { render, screen } from '@testing-library/react';
33
import { useAuthContext } from '../../contexts/AuthContext';
44
import { useLogContext } from '../../contexts/LogContext';
5-
import { AuthTriggers } from '../../lib/auth';
65
import { LogEvent, TargetType } from '../../lib/log';
7-
import { TagSignupBanner } from './TagSignupBanner';
6+
import { TagSignupHero } from './TagSignupHero';
87

98
jest.mock('../../contexts/AuthContext', () => ({
109
useAuthContext: jest.fn(),
1110
}));
1211
jest.mock('../../contexts/LogContext', () => ({
1312
useLogContext: jest.fn(),
1413
}));
14+
jest.mock('../auth/AuthOptions', () => ({
15+
__esModule: true,
16+
default: () => <div data-testid="auth-options" />,
17+
}));
1518

1619
const showLogin = jest.fn();
1720
const logEvent = jest.fn();
@@ -29,34 +32,25 @@ beforeEach(() => {
2932
(useLogContext as jest.Mock).mockReturnValue({ logEvent });
3033
});
3134

32-
describe('TagSignupBanner', () => {
33-
it('renders a signup-first CTA and logs an impression for logged-out users', () => {
35+
describe('TagSignupHero', () => {
36+
it('shows the auth hero and logs an impression for logged-out users', () => {
3437
mockAuth();
35-
render(<TagSignupBanner tag="React" />);
38+
render(<TagSignupHero tag="React" />);
3639

37-
expect(screen.getByRole('heading', { name: /Follow React/ })).toBeVisible();
40+
expect(
41+
screen.getByRole('heading', { name: /make every tab count/i }),
42+
).toBeVisible();
43+
expect(screen.getByTestId('auth-options')).toBeInTheDocument();
3844
expect(logEvent).toHaveBeenCalledWith({
3945
event_name: LogEvent.Impression,
4046
target_type: TargetType.SignupButton,
4147
target_id: 'tag_page',
4248
});
4349
});
4450

45-
it('opens registration on signup click', () => {
46-
mockAuth();
47-
render(<TagSignupBanner tag="React" />);
48-
49-
fireEvent.click(screen.getByRole('button', { name: /Sign up/ }));
50-
51-
expect(showLogin).toHaveBeenCalledWith({
52-
trigger: AuthTriggers.Onboarding,
53-
options: { isLogin: false },
54-
});
55-
});
56-
5751
it('renders nothing for logged-in users', () => {
5852
mockAuth({ isLoggedIn: true });
59-
const { container } = render(<TagSignupBanner tag="React" />);
53+
const { container } = render(<TagSignupHero tag="React" />);
6054

6155
expect(container).toBeEmptyDOMElement();
6256
expect(logEvent).not.toHaveBeenCalled();
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import type { MutableRefObject, ReactElement } from 'react';
2+
import React, { useEffect, useRef } from 'react';
3+
import classNames from 'classnames';
4+
import AuthOptions from '../auth/AuthOptions';
5+
import type { AuthOptionsProps } from '../auth/common';
6+
import { AuthDisplay } from '../auth/common';
7+
import { ButtonSize, ButtonVariant } from '../buttons/Button';
8+
import { useAuthContext } from '../../contexts/AuthContext';
9+
import { useLogContext } from '../../contexts/LogContext';
10+
import { AuthTriggers } from '../../lib/auth';
11+
import { LogEvent, TargetType } from '../../lib/log';
12+
import {
13+
cloudinaryOnboardingFullBackgroundDesktop,
14+
cloudinaryOnboardingFullBackgroundMobile,
15+
} from '../../lib/image';
16+
import {
17+
Typography,
18+
TypographyColor,
19+
TypographyTag,
20+
TypographyType,
21+
} from '../typography/Typography';
22+
23+
interface TagSignupHeroProps {
24+
tag: string;
25+
className?: string;
26+
}
27+
28+
const TARGET_ID = 'tag_page';
29+
30+
// A signup-first hero shown above everything on a tag page for logged-out
31+
// visitors — the same "Where developers make every tab count" auth surface as
32+
// the new-tab hijacking strip (#6127), inline and compact. The social options
33+
// (Google / GitHub / email) come from the shared AuthOptions onboarding-signup
34+
// display. Renders nothing when logged in.
35+
export function TagSignupHero({
36+
tag,
37+
className,
38+
}: TagSignupHeroProps): ReactElement | null {
39+
const { isAuthReady, isLoggedIn, showLogin } = useAuthContext();
40+
const { logEvent } = useLogContext();
41+
const formRef = useRef<HTMLFormElement>(null);
42+
const impressionLogged = useRef(false);
43+
44+
const show = isAuthReady && !isLoggedIn;
45+
46+
useEffect(() => {
47+
if (show && !impressionLogged.current) {
48+
impressionLogged.current = true;
49+
logEvent({
50+
event_name: LogEvent.Impression,
51+
target_type: TargetType.SignupButton,
52+
target_id: TARGET_ID,
53+
});
54+
}
55+
}, [show, logEvent]);
56+
57+
if (!show) {
58+
return null;
59+
}
60+
61+
const onAuthStateUpdate: AuthOptionsProps['onAuthStateUpdate'] = (props) => {
62+
logEvent({
63+
event_name: LogEvent.Click,
64+
target_type: props.isLoginFlow
65+
? TargetType.LoginButton
66+
: TargetType.SignupButton,
67+
target_id: TARGET_ID,
68+
});
69+
showLogin({
70+
trigger: AuthTriggers.Onboarding,
71+
options: {
72+
isLogin: !!props.isLoginFlow,
73+
defaultDisplay: props.defaultDisplay,
74+
formValues: props.email ? { email: props.email } : undefined,
75+
},
76+
});
77+
};
78+
79+
return (
80+
<aside
81+
className={classNames(
82+
'relative overflow-hidden rounded-16 border border-border-subtlest-tertiary',
83+
className,
84+
)}
85+
>
86+
<picture>
87+
<source
88+
media="(max-width: 655px)"
89+
srcSet={cloudinaryOnboardingFullBackgroundMobile}
90+
/>
91+
<source
92+
media="(min-width: 656px)"
93+
srcSet={cloudinaryOnboardingFullBackgroundDesktop}
94+
/>
95+
<img
96+
alt=""
97+
aria-hidden
98+
role="presentation"
99+
src={cloudinaryOnboardingFullBackgroundDesktop}
100+
className="pointer-events-none absolute inset-0 size-full object-cover"
101+
/>
102+
</picture>
103+
<div
104+
aria-hidden
105+
className="pointer-events-none absolute inset-0 bg-overlay-primary-pepper"
106+
/>
107+
<div className="dark relative z-1 mx-auto flex w-full max-w-screen-tablet flex-col items-center gap-4 px-6 py-8 text-center">
108+
<Typography
109+
tag={TypographyTag.H2}
110+
type={TypographyType.Title2}
111+
color={TypographyColor.Primary}
112+
bold
113+
center
114+
>
115+
Where developers make every tab count.
116+
</Typography>
117+
<Typography
118+
type={TypographyType.Callout}
119+
color={TypographyColor.Secondary}
120+
center
121+
className="max-w-[28rem]"
122+
>
123+
Sign up to follow {tag} and turn daily.dev into your personalized
124+
feed.
125+
</Typography>
126+
<div className="w-full max-w-[22rem] rounded-16 border border-border-subtlest-tertiary bg-surface-float p-3 backdrop-blur-md">
127+
<AuthOptions
128+
ignoreMessages
129+
compact
130+
simplified
131+
forceDefaultDisplay
132+
defaultDisplay={AuthDisplay.OnboardingSignup}
133+
trigger={AuthTriggers.Onboarding}
134+
formRef={formRef as unknown as MutableRefObject<HTMLFormElement>}
135+
className={{
136+
container: 'mx-auto !max-w-none !overflow-visible',
137+
onboardingSignup: '!gap-3',
138+
}}
139+
onboardingSignupButton={{
140+
variant: ButtonVariant.Primary,
141+
size: ButtonSize.Large,
142+
}}
143+
onAuthStateUpdate={onAuthStateUpdate}
144+
/>
145+
</div>
146+
</div>
147+
</aside>
148+
);
149+
}

0 commit comments

Comments
 (0)