diff --git a/packages/shared/src/components/filters/IntroQuestButton.spec.tsx b/packages/shared/src/components/filters/IntroQuestButton.spec.tsx index 931716ad374..88ba7b43d7a 100644 --- a/packages/shared/src/components/filters/IntroQuestButton.spec.tsx +++ b/packages/shared/src/components/filters/IntroQuestButton.spec.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { act, render, screen } from '@testing-library/react'; +import { act, fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { useAuthContext } from '../../contexts/AuthContext'; import { useSettingsContext } from '../../contexts/SettingsContext'; @@ -160,6 +160,65 @@ describe('IntroQuestButton', () => { expect(cta).toHaveAttribute('data-expanded', 'false'); }); + it('shakes once the intro CTA collapses and then every 5 seconds', () => { + jest.useFakeTimers(); + + render(); + + const button = screen.getByRole('button', { + name: /Open introduction quests/, + }); + + expect(button).not.toHaveClass('animate-nudge-shake'); + + act(() => { + jest.advanceTimersByTime(2_500); + }); + + expect(button).toHaveClass('animate-nudge-shake'); + + act(() => { + jest.advanceTimersByTime(600); + }); + + expect(button).not.toHaveClass('animate-nudge-shake'); + + act(() => { + jest.advanceTimersByTime(4_400); + }); + + expect(button).toHaveClass('animate-nudge-shake'); + }); + + it('stops shaking once the button is clicked', () => { + jest.useFakeTimers(); + + render(); + + const button = screen.getByRole('button', { + name: /Open introduction quests/, + }); + + act(() => { + jest.advanceTimersByTime(2_500); + }); + + expect(button).toHaveClass('animate-nudge-shake'); + + act(() => { + fireEvent.click(button); + }); + + expect(openModal).toHaveBeenCalledWith({ type: LazyModal.IntroQuests }); + expect(button).not.toHaveClass('animate-nudge-shake'); + + act(() => { + jest.advanceTimersByTime(30_000); + }); + + expect(button).not.toHaveClass('animate-nudge-shake'); + }); + it('hides the badge after intro quests have been viewed and none are claimable', () => { mockUseActions.mockReturnValue({ checkHasCompleted: jest.fn( diff --git a/packages/shared/src/components/filters/IntroQuestButton.tsx b/packages/shared/src/components/filters/IntroQuestButton.tsx index fff5da67b9f..ebe97b023e9 100644 --- a/packages/shared/src/components/filters/IntroQuestButton.tsx +++ b/packages/shared/src/components/filters/IntroQuestButton.tsx @@ -17,6 +17,9 @@ import { QuestStatus } from '../../graphql/quests'; const INTRO_QUEST_CTA = 'Get the most out of daily.dev'; const INTRO_QUEST_CTA_DURATION_MS = 2000; +const INTRO_QUEST_CTA_COLLAPSE_MS = 500; +const NUDGE_SHAKE_INTERVAL_MS = 5_000; +const NUDGE_SHAKE_DURATION_MS = 600; export function IntroQuestButton(): ReactElement | null { const { isAuthReady, isLoggedIn } = useAuthContext(); @@ -37,7 +40,10 @@ export function IntroQuestButton(): ReactElement | null { ActionType.IntroQuestsCompleted, ); const [isIntroCtaVisible, setIsIntroCtaVisible] = useState(false); + const [hasIntroCtaFinished, setHasIntroCtaFinished] = useState(false); + const [isShaking, setIsShaking] = useState(false); const hasShownIntroCta = useRef(false); + const hasClickedRef = useRef(false); const shouldRenderButton = isAuthReady && loadedSettings && @@ -56,14 +62,40 @@ export function IntroQuestButton(): ReactElement | null { hasShownIntroCta.current = true; setIsIntroCtaVisible(true); - const timeout = setTimeout( + const visibilityTimeout = setTimeout( () => setIsIntroCtaVisible(false), INTRO_QUEST_CTA_DURATION_MS, ); + const finishedTimeout = setTimeout( + () => setHasIntroCtaFinished(true), + INTRO_QUEST_CTA_DURATION_MS + INTRO_QUEST_CTA_COLLAPSE_MS, + ); - return () => clearTimeout(timeout); + return () => { + clearTimeout(visibilityTimeout); + clearTimeout(finishedTimeout); + }; }, [shouldRenderButton]); + useEffect(() => { + if (!shouldRenderButton || !hasIntroCtaFinished || hasClickedRef.current) { + return undefined; + } + + const triggerShake = () => { + if (hasClickedRef.current) { + return; + } + setIsShaking(true); + setTimeout(() => setIsShaking(false), NUDGE_SHAKE_DURATION_MS); + }; + + triggerShake(); + const interval = setInterval(triggerShake, NUDGE_SHAKE_INTERVAL_MS); + + return () => clearInterval(interval); + }, [shouldRenderButton, hasIntroCtaFinished]); + if (!shouldRenderButton) { return null; } @@ -77,6 +109,12 @@ export function IntroQuestButton(): ReactElement | null { const buttonLabel = `${completed}/${introQuests.length}`; const buttonVariant = isLaptop ? ButtonVariant.Float : ButtonVariant.Tertiary; + const handleClick = () => { + hasClickedRef.current = true; + setIsShaking(false); + openModal({ type: LazyModal.IntroQuests }); + }; + return (