From 6e022614b8721cc54033158fced0edd009b91727 Mon Sep 17 00:00:00 2001 From: davidercruz Date: Tue, 14 Apr 2026 11:23:41 +0100 Subject: [PATCH 1/9] feat(onboarding): add results screen, tag chips, and lower swipe milestones Co-Authored-By: Claude Opus 4.6 --- .../modals/hotTakes/HotAndColdModal.tsx | 12 ++++ .../webapp/lib/swipeOnboardingGuidance.ts | 18 +++--- packages/webapp/pages/onboarding/swipe.tsx | 61 ++++++++++++++++++- 3 files changed, 79 insertions(+), 12 deletions(-) diff --git a/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx b/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx index d58821bd58..0e146c15af 100644 --- a/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx +++ b/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx @@ -1433,6 +1433,18 @@ const OnboardingPostCard = ({ {card.title || 'Popular developer story'} + {card.tags && card.tags.length > 0 && ( +
+ {card.tags.slice(0, 5).map((tag) => ( + + {tag} + + ))} +
+ )}
{card.image ? ( ('prompt'); const [promptText, setPromptText] = useState(''); const [promptLoading, setPromptLoading] = useState(false); @@ -313,14 +313,69 @@ function SwipeOnboardingPage(): ReactElement { variant={ButtonVariant.Primary} type="button" onClick={() => { - onComplete().catch(() => null); + setOnboardingUiMode('results'); }} > - Go to my feed + See my interests
) : null; + if (onboardingUiMode === 'results') { + return ( +
+
+

+ Your interests +

+

+ Based on your swipes, we selected these tags for your feed. +

+ {adaptiveSelectedTags.length > 0 ? ( +
+ {adaptiveSelectedTags.map((tag) => ( + + {tag} + + ))} +
+ ) : ( +

+ No tags selected yet. Swipe right on posts you find interesting! +

+ )} +
+ + +
+
+
+ ); + } + if (onboardingUiMode === 'prompt') { return (
From 709699c637a579e26f54dbca89b2f00d5f7408d4 Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Wed, 15 Apr 2026 11:36:00 +0200 Subject: [PATCH 2/9] feat: add tldr, make chatbox cleaner --- .../modals/hotTakes/HotAndColdModal.tsx | 82 ++- .../SwipeOnboardingProgressHeader.tsx | 25 +- packages/webapp/hooks/useAdaptiveSwipeDeck.ts | 1 + packages/webapp/pages/onboarding/swipe.tsx | 579 +++++++++++------- 4 files changed, 436 insertions(+), 251 deletions(-) diff --git a/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx b/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx index 0e146c15af..7fe109a2e9 100644 --- a/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx +++ b/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx @@ -1438,23 +1438,50 @@ const OnboardingPostCard = ({ {card.tags.slice(0, 5).map((tag) => ( {tag} ))}
)} -
- {card.image ? ( - {card.title +
+ {card.summary ? ( + <> + + TLDR + + + {card.summary} + + ) : ( -
+ <> + + TLDR + + + No summary available for this post yet. + + )}
@@ -1470,9 +1497,7 @@ const OnboardingFeedEmptyState = ({ isRefetching: boolean; }): ReactElement => (
- {isRefetching ? ( - - ) : null} + {isRefetching ? : null} } {headerSlot} @@ -2167,18 +2193,20 @@ const HotAndColdModal = ({ <> {!isOnboardingMode && topSlot} {isOnboardingMode ? ( -
- {topSlot} - {cardSwipeArea} -
- handleDismiss('right', 'button')} - onNotInteresting={() => handleDismiss('left', 'button')} - /> +
+
+ {topSlot} +
{cardSwipeArea}
+
+ handleDismiss('right', 'button')} + onNotInteresting={() => handleDismiss('left', 'button')} + /> +
+ {bottomSlot}
- {bottomSlot}
) : ( <> diff --git a/packages/webapp/components/onboarding/SwipeOnboardingProgressHeader.tsx b/packages/webapp/components/onboarding/SwipeOnboardingProgressHeader.tsx index 64ff996b48..a520332d70 100644 --- a/packages/webapp/components/onboarding/SwipeOnboardingProgressHeader.tsx +++ b/packages/webapp/components/onboarding/SwipeOnboardingProgressHeader.tsx @@ -10,16 +10,16 @@ import { getSwipeOnboardingBarProgress, getSwipeOnboardingGuidanceMessage, getSwipeOnboardingHeadline, + SWIPE_ONBOARDING_REFINE_TARGET, type SwipeOnboardingProgressCopyVariant, } from '../../lib/swipeOnboardingGuidance'; /** Typing speed; full headline refresh when swipe tier copy changes. */ const SWIPE_HEADLINE_TYPING_MS_PER_CHAR = 12; /** - * Stable min height = 3 × typo-title2 line-height (1.875rem) so headline changes do not - * shift the progress bar. + * Stable min height keeps the typed copy from jumping while the progress bar updates. */ -const SWIPE_HEADLINE_BLOCK_MIN_HEIGHT_CLASS = 'min-h-[5.625rem]'; +const SWIPE_HEADLINE_BLOCK_MIN_HEIGHT_CLASS = 'min-h-[4.75rem]'; function SwipeOnboardingTypingHeadline({ line1, @@ -59,14 +59,14 @@ function SwipeOnboardingTypingHeadline({ return (
{shownLine1} {shownLine2 !== undefined ? ( @@ -117,9 +117,11 @@ export function SwipeOnboardingProgressHeader({ const progress = getSwipeOnboardingBarProgress(progressCount); const { line1: headlineLine1, line2: headlineLine2 } = getSwipeOnboardingHeadline(progressCount, copyVariant); + const progressLabel = copyVariant === 'tags' ? 'Manual setup' : 'Feed setup'; + const progressValue = Math.min(progressCount, SWIPE_ONBOARDING_REFINE_TARGET); return ( -
+
{/* eslint-disable-next-line react/no-unknown-property -- scoped keyframes for progress bar */} +
+ + {progressLabel} + + + {progressValue} / {SWIPE_ONBOARDING_REFINE_TARGET} + +
+
+ {children} +
+
+ ); +} + +function SwipeOnboardingToolbar({ + label, + actionLabel, + onAction, + onBack, +}: { + label: string; + actionLabel: string; + onAction: () => void; + onBack: () => void; +}): ReactElement { + return ( +
+
+ +
+
+ ); +} + function SwipeOnboardingPage(): ReactElement { const router = useRouter(); const formRef = useRef(null as unknown as HTMLFormElement); @@ -105,7 +181,6 @@ function SwipeOnboardingPage(): ReactElement { handleSwipe: handleAdaptiveSwipe, retryFetch, selectedTags: adaptiveSelectedTags, - rightSwipedPostIds, } = useAdaptiveSwipeDeck(); const handlePromptSubmit = useCallback(async () => { @@ -289,13 +364,38 @@ function SwipeOnboardingPage(): ReactElement { if (!isLoggedIn) { return ( -
- -
- + +
+
- -
+
+
+
+
+ + Personalize your daily.dev feed + +
+

+ Sign in to start shaping your feed +

+

+ We'll turn your interests and swipes into a smarter + starter feed in just a few steps. +

+
+
+ +
+
+
+ + ); } @@ -307,245 +407,292 @@ function SwipeOnboardingPage(): ReactElement { const bottomContinueSlot = canContinue ? (
- +
+
+

+ Starter feed ready +

+

+ We have enough signal to build your first pass. You can keep + refining it after this. +

+
+ +
) : null; if (onboardingUiMode === 'results') { return ( -
-
-

- Your interests -

-

- Based on your swipes, we selected these tags for your feed. -

- {adaptiveSelectedTags.length > 0 ? ( -
- {adaptiveSelectedTags.map((tag) => ( - - {tag} + +
+
+
+
+ + Your starter feed - ))} +
+

+ Your interests are taking shape +

+

+ Based on your swipes, we pulled together the topics most + likely to improve your feed right away. +

+
+
+
+ {adaptiveSelectedTags.length > 0 ? ( +
+ {adaptiveSelectedTags.map((tag) => ( + + {tag} + + ))} +
+ ) : ( +

+ No tags selected yet. Swipe right on posts you find + interesting to train the feed. +

+ )} +
+ + +
+
- ) : ( -

- No tags selected yet. Swipe right on posts you find interesting! -

- )} -
- -
-
+
); } if (onboardingUiMode === 'prompt') { return ( -
-
-

- What are you interested in? -

-

- Describe your interests and we'll find the best content for - you. -

-