Skip to content

Commit f79c114

Browse files
committed
add get more out of daily dev text
1 parent 2fa527b commit f79c114

2 files changed

Lines changed: 100 additions & 8 deletions

File tree

packages/shared/src/components/filters/IntroQuestButton.spec.tsx

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { render, screen } from '@testing-library/react';
2+
import { act, render, screen } from '@testing-library/react';
33
import userEvent from '@testing-library/user-event';
44
import { useAuthContext } from '../../contexts/AuthContext';
55
import { useSettingsContext } from '../../contexts/SettingsContext';
@@ -122,6 +122,7 @@ describe('IntroQuestButton', () => {
122122
});
123123

124124
afterEach(() => {
125+
jest.useRealTimers();
125126
jest.clearAllMocks();
126127
});
127128

@@ -144,6 +145,23 @@ describe('IntroQuestButton', () => {
144145
});
145146
});
146147

148+
it('shows a CTA on load and retracts it after 2 seconds', () => {
149+
jest.useFakeTimers();
150+
151+
render(<IntroQuestButton />);
152+
153+
const cta = screen.getByTestId('intro-quest-cta');
154+
155+
expect(cta).toHaveTextContent('Get the most out of daily.dev');
156+
expect(cta).toHaveAttribute('data-expanded', 'true');
157+
158+
act(() => {
159+
jest.advanceTimersByTime(2000);
160+
});
161+
162+
expect(cta).toHaveAttribute('data-expanded', 'false');
163+
});
164+
147165
it('hides the badge after intro quests have been viewed and none are claimable', () => {
148166
mockUseActions.mockReturnValue({
149167
checkHasCompleted: jest.fn(
@@ -222,4 +240,31 @@ describe('IntroQuestButton', () => {
222240
screen.queryByRole('button', { name: /Open introduction quests/ }),
223241
).not.toBeInTheDocument();
224242
});
243+
244+
it('does not render when all intro quests are claimed', () => {
245+
mockUseQuestDashboard.mockReturnValue({
246+
data: {
247+
intro: [
248+
buildIntroQuest({
249+
rotationId: 'rot-1',
250+
status: QuestStatus.Claimed,
251+
completedAt: '2026-05-03T10:00:00.000Z',
252+
claimedAt: '2026-05-03T10:05:00.000Z',
253+
}),
254+
buildIntroQuest({
255+
rotationId: 'rot-2',
256+
status: QuestStatus.Claimed,
257+
completedAt: '2026-05-03T10:10:00.000Z',
258+
claimedAt: '2026-05-03T10:15:00.000Z',
259+
}),
260+
],
261+
},
262+
});
263+
264+
render(<IntroQuestButton />);
265+
266+
expect(
267+
screen.queryByRole('button', { name: /Open introduction quests/ }),
268+
).not.toBeInTheDocument();
269+
});
225270
});

packages/shared/src/components/filters/IntroQuestButton.tsx

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ReactElement } from 'react';
2-
import React from 'react';
2+
import React, { useEffect, useRef, useState } from 'react';
3+
import classNames from 'classnames';
34
import { Button, ButtonSize, ButtonVariant } from '../buttons/Button';
45
import { TourIcon } from '../icons';
56
import { Tooltip } from '../tooltip/Tooltip';
@@ -15,6 +16,9 @@ import { featureNewD1Experience } from '../../lib/featureManagement';
1516
import { useQuestDashboard } from '../../hooks/useQuestDashboard';
1617
import { QuestStatus } from '../../graphql/quests';
1718

19+
const INTRO_QUEST_CTA = 'Get the most out of daily.dev';
20+
const INTRO_QUEST_CTA_DURATION_MS = 2000;
21+
1822
export function IntroQuestButton(): ReactElement | null {
1923
const { isAuthReady, isLoggedIn } = useAuthContext();
2024
const { loadedSettings } = useSettingsContext();
@@ -27,13 +31,38 @@ export function IntroQuestButton(): ReactElement | null {
2731
});
2832
const { data } = useQuestDashboard();
2933
const introQuests = data?.intro ?? [];
34+
const allIntroQuestsClaimed =
35+
introQuests.length > 0 &&
36+
introQuests.every(({ status }) => status === QuestStatus.Claimed);
3037
const hasViewedIntroQuests = checkHasCompleted(ActionType.ViewedIntroQuests);
38+
const [isIntroCtaVisible, setIsIntroCtaVisible] = useState(false);
39+
const hasShownIntroCta = useRef(false);
40+
const shouldRenderButton =
41+
isAuthReady &&
42+
loadedSettings &&
43+
isLoggedIn &&
44+
isNewD1Experience &&
45+
introQuests.length > 0 &&
46+
!allIntroQuestsClaimed;
47+
const showIntroCta =
48+
shouldRenderButton && (!hasShownIntroCta.current || isIntroCtaVisible);
3149

32-
if (!isAuthReady || !loadedSettings || !isLoggedIn || !isNewD1Experience) {
33-
return null;
34-
}
50+
useEffect(() => {
51+
if (!shouldRenderButton || hasShownIntroCta.current) {
52+
return undefined;
53+
}
3554

36-
if (introQuests.length === 0) {
55+
hasShownIntroCta.current = true;
56+
setIsIntroCtaVisible(true);
57+
const timeout = setTimeout(
58+
() => setIsIntroCtaVisible(false),
59+
INTRO_QUEST_CTA_DURATION_MS,
60+
);
61+
62+
return () => clearTimeout(timeout);
63+
}, [shouldRenderButton]);
64+
65+
if (!shouldRenderButton) {
3766
return null;
3867
}
3968

@@ -44,12 +73,13 @@ export function IntroQuestButton(): ReactElement | null {
4473
const hasClaimableIntroQuest = introQuests.some(({ claimable }) => claimable);
4574
const showAttentionBadge = !hasViewedIntroQuests || hasClaimableIntroQuest;
4675
const buttonLabel = `${completed}/${introQuests.length}`;
76+
const buttonVariant = isLaptop ? ButtonVariant.Float : ButtonVariant.Tertiary;
4777

4878
return (
4979
<Tooltip content="Introduction" side="bottom">
5080
<Button
5181
type="button"
52-
variant={isLaptop ? ButtonVariant.Float : ButtonVariant.Tertiary}
82+
variant={buttonVariant}
5383
size={ButtonSize.Medium}
5484
icon={<TourIcon />}
5585
className="relative"
@@ -66,7 +96,24 @@ export function IntroQuestButton(): ReactElement | null {
6696
!
6797
</Bubble>
6898
)}
69-
{buttonLabel}
99+
<span className="flex min-w-0 items-center">
100+
<span
101+
aria-hidden="true"
102+
data-testid="intro-quest-cta"
103+
data-expanded={showIntroCta ? 'true' : 'false'}
104+
className={classNames(
105+
'inline-flex shrink-0 items-center overflow-hidden whitespace-nowrap rounded-10 transition-all duration-500 ease-out motion-reduce:transition-none',
106+
showIntroCta
107+
? 'mr-2 max-w-[18rem] bg-background-subtle px-3 py-1 opacity-100'
108+
: 'mr-0 max-w-0 bg-background-subtle px-0 py-0 opacity-0',
109+
)}
110+
>
111+
<span className="min-w-max font-bold typo-footnote">
112+
{INTRO_QUEST_CTA}
113+
</span>
114+
</span>
115+
<span className="shrink-0">{buttonLabel}</span>
116+
</span>
70117
</Button>
71118
</Tooltip>
72119
);

0 commit comments

Comments
 (0)