diff --git a/packages/shared/src/components/auth/AuthOptionsInner.tsx b/packages/shared/src/components/auth/AuthOptionsInner.tsx index 3422fff9468..21d8d0f5789 100644 --- a/packages/shared/src/components/auth/AuthOptionsInner.tsx +++ b/packages/shared/src/components/auth/AuthOptionsInner.tsx @@ -159,6 +159,8 @@ function AuthOptionsInner({ simplified = false, ignoreMessages = false, onboardingSignupButton, + autoTriggerProvider, + socialProviderScopes, }: AuthOptionsProps): ReactElement { const { displayToast } = useToastNotification(); const { syncSettings } = useSettingsContext(); @@ -520,6 +522,7 @@ function AuthOptionsInner({ provider.toLowerCase(), callbackURL, additionalData, + socialProviderScopes, ); if (!socialUrl) { logEvent({ @@ -558,6 +561,21 @@ function AuthOptionsInner({ onAuthStateUpdate?.({ isLoading: true }); }; + const onProviderClickRef = useRef(onProviderClick); + onProviderClickRef.current = onProviderClick; + const autoTriggerFiredProvider = useRef(null); + + useEffect(() => { + if ( + !autoTriggerProvider || + autoTriggerFiredProvider.current === autoTriggerProvider + ) { + return; + } + autoTriggerFiredProvider.current = autoTriggerProvider; + onProviderClickRef.current(autoTriggerProvider, false); + }, [autoTriggerProvider]); + const onProviderMessage = async (e: MessageEvent) => { if (checkIsLoginMessage(e)) { return handleLoginMessage(e); diff --git a/packages/shared/src/components/auth/OnboardingRegistrationForm.tsx b/packages/shared/src/components/auth/OnboardingRegistrationForm.tsx index e92ea9f8846..f2e434da1be 100644 --- a/packages/shared/src/components/auth/OnboardingRegistrationForm.tsx +++ b/packages/shared/src/components/auth/OnboardingRegistrationForm.tsx @@ -5,7 +5,7 @@ import { providerMap } from './common'; import OrDivider from './OrDivider'; import { useLogContext } from '../../contexts/LogContext'; import type { AuthTriggersType } from '../../lib/auth'; -import { AuthEventNames } from '../../lib/auth'; +import { AuthEventNames, AuthTriggers } from '../../lib/auth'; import type { ButtonProps } from '../buttons/Button'; import { Button, ButtonSize, ButtonVariant } from '../buttons/Button'; import { isIOSNative } from '../../lib/func'; @@ -13,6 +13,8 @@ import { isIOSNative } from '../../lib/func'; import { MemberAlready } from '../onboarding/MemberAlready'; import SignupDisclaimer from './SignupDisclaimer'; import { FunnelTargetId } from '../../features/onboarding/types/funnelEvents'; +import { useConditionalFeature } from '../../hooks/useConditionalFeature'; +import { featureOnboardingV2 } from '../../lib/featureManagement'; interface ClassName { onboardingSignup?: string; @@ -105,6 +107,10 @@ export const OnboardingRegistrationForm = ({ trigger, }: OnboardingRegistrationFormProps): ReactElement => { const { logEvent } = useLogContext(); + const { value: isOnboardingV2 } = useConditionalFeature({ + feature: featureOnboardingV2, + shouldEvaluate: trigger === AuthTriggers.Onboarding, + }); const trackOpenSignup = () => { logEvent({ @@ -157,8 +163,9 @@ export const OnboardingRegistrationForm = ({ onExistingEmail?.('')} className={{ - container: - 'mx-auto mt-6 text-center text-text-secondary typo-callout', + container: isOnboardingV2 + ? 'mx-auto mt-6 w-full justify-center border-t border-border-subtlest-tertiary pt-6 text-center text-text-secondary typo-callout' + : 'mx-auto mt-6 text-center text-text-secondary typo-callout', login: '!text-inherit', }} /> diff --git a/packages/shared/src/components/auth/RegistrationForm.tsx b/packages/shared/src/components/auth/RegistrationForm.tsx index ae11d36198a..7305a8c0271 100644 --- a/packages/shared/src/components/auth/RegistrationForm.tsx +++ b/packages/shared/src/components/auth/RegistrationForm.tsx @@ -10,6 +10,8 @@ import type { RegistrationParameters, } from '../../lib/auth'; import { AuthEventNames, AuthTriggers } from '../../lib/auth'; +import { useConditionalFeature } from '../../hooks/useConditionalFeature'; +import { featureOnboardingV2 } from '../../lib/featureManagement'; import { formToJson } from '../../lib/form'; import { Button, ButtonVariant, ButtonSize } from '../buttons/Button'; import { PasswordField } from '../fields/PasswordField'; @@ -82,6 +84,11 @@ const RegistrationForm = ({ const [isSubmitted, setIsSubmitted] = useState(false); const [name, setName] = useState(''); const isRecruiterOnboarding = trigger === AuthTriggers.RecruiterSelfServe; + const { value: isOnboardingV2 } = useConditionalFeature({ + feature: featureOnboardingV2, + shouldEvaluate: trigger === AuthTriggers.Onboarding, + }); + const hideExperienceLevel = isRecruiterOnboarding || isOnboardingV2; const { username, setUsername, @@ -163,7 +170,7 @@ const RegistrationForm = ({ ); delete values['cf-turnstile-response']; - const requiresExperienceLevel = !isRecruiterOnboarding; + const requiresExperienceLevel = !hideExperienceLevel; if ( !values['traits.name']?.length || !values['traits.username']?.length || @@ -275,9 +282,16 @@ const RegistrationForm = ({ variant={ButtonVariant.Secondary} /> Join daily.dev @@ -396,7 +410,7 @@ const RegistrationForm = ({ } rightIcon={usernameIcon} /> - {!isRecruiterOnboarding && ( + {!hideExperienceLevel && ( { const { logEvent } = useLogContext(); const { user } = useContext(AuthContext); + const { value: isOnboardingV2 } = useConditionalFeature({ + feature: featureOnboardingV2, + shouldEvaluate: trigger === AuthTriggers.Onboarding, + }); + const hideExperienceLevel = isOnboardingV2; const [nameHint, setNameHint] = useState(null); const [usernameHint, setUsernameHint] = useState(null); const [experienceLevelHint, setExperienceLevelHint] = useState(null); @@ -118,7 +126,7 @@ export const SocialRegistrationForm = ({ return; } - if (!values.experienceLevel?.length) { + if (!hideExperienceLevel && !values.experienceLevel?.length) { logError('Experience level not provided'); setExperienceLevelHint('Please select your experience level'); return; @@ -229,18 +237,20 @@ export const SocialRegistrationForm = ({ } rightIcon={isLoadingUsername ? : null} /> - { - if (experienceLevelHint) { - setExperienceLevelHint(null); - } - }} - valid={experienceLevelHint === null} - hint={experienceLevelHint} - saveHintSpace - /> + {!hideExperienceLevel && ( + { + if (experienceLevelHint) { + setExperienceLevelHint(null); + } + }} + valid={experienceLevelHint === null} + hint={experienceLevelHint} + saveHintSpace + /> + )} Your email will be used to send you product and community updates diff --git a/packages/shared/src/components/auth/common.tsx b/packages/shared/src/components/auth/common.tsx index cfb59e49373..f784a698b1e 100644 --- a/packages/shared/src/components/auth/common.tsx +++ b/packages/shared/src/components/auth/common.tsx @@ -42,7 +42,7 @@ export const providerMap: ProviderMap = { value: 'google', }, github: { - icon: , + icon: , label: 'GitHub', value: 'github', }, @@ -130,4 +130,6 @@ export interface AuthOptionsProps { targetId?: string; ignoreMessages?: boolean; onboardingSignupButton?: ButtonProps<'button'>; + autoTriggerProvider?: string; + socialProviderScopes?: string[]; } diff --git a/packages/shared/src/components/icons/GitHub/filled.svg b/packages/shared/src/components/icons/GitHub/filled.svg new file mode 100644 index 00000000000..a1fc07139bb --- /dev/null +++ b/packages/shared/src/components/icons/GitHub/filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/shared/src/components/icons/GitHub/index.tsx b/packages/shared/src/components/icons/GitHub/index.tsx index d322e269807..43926cb3bce 100644 --- a/packages/shared/src/components/icons/GitHub/index.tsx +++ b/packages/shared/src/components/icons/GitHub/index.tsx @@ -3,7 +3,8 @@ import React from 'react'; import type { IconProps } from '../../Icon'; import Icon from '../../Icon'; import WhiteIcon from './white.svg'; +import FilledIcon from './filled.svg'; export const GitHubIcon = (props: IconProps): ReactElement => ( - + ); diff --git a/packages/shared/src/graphql/onboardingProfileTags.ts b/packages/shared/src/graphql/onboardingProfileTags.ts new file mode 100644 index 00000000000..f3d5e2d8db3 --- /dev/null +++ b/packages/shared/src/graphql/onboardingProfileTags.ts @@ -0,0 +1,38 @@ +import { gql } from 'graphql-request'; +import { gqlClient } from './common'; + +export type OnboardingTagsResult = { + includeTags: string[]; +}; + +export const GITHUB_PROFILE_TAGS_MUTATION = gql` + mutation GitHubProfileTags { + githubProfileTags { + includeTags + } + } +`; + +export const ONBOARDING_PROFILE_TAGS_MUTATION = gql` + mutation OnboardingProfileTags($prompt: String!) { + onboardingProfileTags(prompt: $prompt) { + includeTags + } + } +`; + +export const requestGitHubProfileTags = async (): Promise => { + const res = await gqlClient.request<{ + githubProfileTags: OnboardingTagsResult; + }>(GITHUB_PROFILE_TAGS_MUTATION); + return res.githubProfileTags.includeTags; +}; + +export const requestOnboardingProfileTags = async ( + prompt: string, +): Promise => { + const res = await gqlClient.request<{ + onboardingProfileTags: OnboardingTagsResult; + }>(ONBOARDING_PROFILE_TAGS_MUTATION, { prompt }); + return res.onboardingProfileTags.includeTags; +}; diff --git a/packages/shared/src/lib/betterAuth.ts b/packages/shared/src/lib/betterAuth.ts index dd7c2636db0..cd35796ebb0 100644 --- a/packages/shared/src/lib/betterAuth.ts +++ b/packages/shared/src/lib/betterAuth.ts @@ -149,6 +149,7 @@ const getBetterAuthSocialRedirect = async ( provider: string, callbackURL: string, additionalData?: SocialAdditionalData, + scopes?: string[], ): Promise => { const absoluteCallbackURL = callbackURL.startsWith('http') ? callbackURL @@ -166,6 +167,7 @@ const getBetterAuthSocialRedirect = async ( callbackURL: absoluteCallbackURL, disableRedirect: true, ...(additionalData && { additionalData }), + ...(scopes?.length && { scopes }), }, 'Failed to get social auth URL', ); @@ -181,12 +183,14 @@ export const getBetterAuthSocialRedirectData = ( provider: string, callbackURL: string, additionalData?: SocialAdditionalData, + scopes?: string[], ): Promise => getBetterAuthSocialRedirect( 'sign-in/social', provider, callbackURL, additionalData, + scopes, ); export const getBetterAuthSocialUrl = ( diff --git a/packages/shared/src/lib/constants.ts b/packages/shared/src/lib/constants.ts index 9233eada7be..729768e17be 100644 --- a/packages/shared/src/lib/constants.ts +++ b/packages/shared/src/lib/constants.ts @@ -33,6 +33,7 @@ export const slackIntegration = 'https://r.daily.dev/slack'; export const statusPage = 'https://r.daily.dev/status'; export const businessWebsiteUrl = 'https://r.daily.dev/business'; export const appsUrl = 'https://daily.dev/apps'; +export const mobileAppUrl = 'https://api.daily.dev/mobile'; export const timezoneSettingsUrl = 'https://r.daily.dev/timezone'; export const isDevelopment = process.env.NODE_ENV === 'development'; export const isProductionAPI = diff --git a/packages/shared/src/lib/featureManagement.ts b/packages/shared/src/lib/featureManagement.ts index db3a5cf33cf..e74dfdaa5b2 100644 --- a/packages/shared/src/lib/featureManagement.ts +++ b/packages/shared/src/lib/featureManagement.ts @@ -152,6 +152,8 @@ export const sharedPostPreviewFeature = new Feature( false, ); +export const featureOnboardingV2 = new Feature('onboarding_v2', isDevelopment); + export const featureUpvoteCountThreshold = new Feature<{ threshold: number; belowThresholdLabel: string; diff --git a/packages/webapp/components/onboarding/OnboardingV2.tsx b/packages/webapp/components/onboarding/OnboardingV2.tsx new file mode 100644 index 00000000000..2794d6bbb41 --- /dev/null +++ b/packages/webapp/components/onboarding/OnboardingV2.tsx @@ -0,0 +1,2466 @@ +import type { ReactElement } from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import classNames from 'classnames'; +import MainFeedLayout from '@dailydotdev/shared/src/components/MainFeedLayout'; +import { FeedLayoutProvider } from '@dailydotdev/shared/src/contexts/FeedContext'; +import { SearchProvider } from '@dailydotdev/shared/src/contexts/search/SearchContext'; +import { ActiveFeedNameContext } from '@dailydotdev/shared/src/contexts/ActiveFeedNameContext'; +import { SharedFeedPage } from '@dailydotdev/shared/src/components/utilities/common'; +import { + ThemeMode, + useSettingsContext, +} from '@dailydotdev/shared/src/contexts/SettingsContext'; +import { useAuthContext } from '@dailydotdev/shared/src/contexts/AuthContext'; +import { + downloadBrowserExtension, + mobileAppUrl, + webappUrl, +} from '@dailydotdev/shared/src/lib/constants'; +import { UserExperienceLevel } from '@dailydotdev/shared/src/lib/user'; +import { AuthTriggers } from '@dailydotdev/shared/src/lib/auth'; + +import { ChromeIcon } from '@dailydotdev/shared/src/components/icons/Browser/Chrome'; +import { MagicIcon } from '@dailydotdev/shared/src/components/icons/Magic'; +import { NewTabIcon } from '@dailydotdev/shared/src/components/icons/NewTab'; +import { TerminalIcon } from '@dailydotdev/shared/src/components/icons/Terminal'; +import { HomeIcon } from '@dailydotdev/shared/src/components/icons/Home'; +import { HotIcon } from '@dailydotdev/shared/src/components/icons/Hot'; +import { EyeIcon } from '@dailydotdev/shared/src/components/icons/Eye'; +import { SquadIcon } from '@dailydotdev/shared/src/components/icons/Squad'; +import { VIcon } from '@dailydotdev/shared/src/components/icons/V'; +import { StarIcon } from '@dailydotdev/shared/src/components/icons/Star'; +import { IconSize } from '@dailydotdev/shared/src/components/Icon'; +import Logo, { LogoPosition } from '@dailydotdev/shared/src/components/Logo'; +import { FooterLinks } from '@dailydotdev/shared/src/components/footer/FooterLinks'; +import AuthOptions from '@dailydotdev/shared/src/components/auth/AuthOptions'; +import type { AuthProps } from '@dailydotdev/shared/src/components/auth/common'; +import { AuthDisplay } from '@dailydotdev/shared/src/components/auth/common'; +import { useRouter } from 'next/router'; +import { + requestGitHubProfileTags, + requestOnboardingProfileTags, +} from '@dailydotdev/shared/src/graphql/onboardingProfileTags'; +import { useActions } from '@dailydotdev/shared/src/hooks'; +import usePersistentContext from '@dailydotdev/shared/src/hooks/usePersistentContext'; +import { ActionType } from '@dailydotdev/shared/src/graphql/actions'; +import { useOnboardingActions } from '@dailydotdev/shared/src/hooks/auth'; + +import { UPDATE_USER_PROFILE_MUTATION } from '@dailydotdev/shared/src/graphql/users'; +import { gqlClient } from '@dailydotdev/shared/src/graphql/common'; +import { redirectToApp } from '@dailydotdev/shared/src/features/onboarding/lib/utils'; +import { GitHubIcon } from '@dailydotdev/shared/src/components/icons/GitHub'; +import { OnboardingV2Styles } from './OnboardingV2Styles'; +import { useOnboardingAnimations } from './useOnboardingAnimations'; + +type RisingTag = { + label: string; + left: string; + delay: string; + duration: string; + driftX: number; +}; + +const RISING_TAGS_DESKTOP: RisingTag[] = [ + { label: 'React', left: '8%', delay: '0s', duration: '14s', driftX: 12 }, + { label: 'AI & ML', left: '28%', delay: '1.2s', duration: '15s', driftX: -8 }, + { + label: 'System Design', + left: '52%', + delay: '0.6s', + duration: '14.5s', + driftX: 10, + }, + { label: 'Docker', left: '78%', delay: '2s', duration: '13.8s', driftX: -14 }, + { + label: 'TypeScript', + left: '18%', + delay: '3.4s', + duration: '15.2s', + driftX: 8, + }, + { + label: 'Next.js', + left: '88%', + delay: '2.8s', + duration: '14.4s', + driftX: -10, + }, + { + label: 'Python', + left: '42%', + delay: '4.2s', + duration: '14.8s', + driftX: -6, + }, + { + label: 'Kubernetes', + left: '66%', + delay: '5s', + duration: '14.2s', + driftX: 12, + }, +]; + +const RISING_TAGS_MOBILE: RisingTag[] = [ + { label: 'React', left: '10%', delay: '0s', duration: '13.5s', driftX: 8 }, + { + label: 'AI & ML', + left: '55%', + delay: '1.5s', + duration: '14s', + driftX: -10, + }, + { label: 'Docker', left: '30%', delay: '3s', duration: '13s', driftX: 6 }, + { + label: 'TypeScript', + left: '75%', + delay: '4.5s', + duration: '14.5s', + driftX: -8, + }, +]; + +export type OnboardingStep = + | 'hero' + | 'prompt' + | 'chooser' + | 'auth' + | 'importing' + | 'extension' + | 'complete'; + +type GithubImportPhase = + | 'idle' + | 'running' + | 'awaitingSeniority' + | 'confirmingSeniority' + | 'finishing' + | 'complete'; +type ImportFlowSource = 'github' | 'ai'; +type GithubImportBodyPhase = 'checklist' | 'seniority' | 'default'; + +const AI_IMPORT_STEPS = [ + { label: 'Analyzing your profile', threshold: 12 }, + { label: 'Matching interests', threshold: 30 }, + { label: 'Mapping your stack', threshold: 46 }, + { label: 'Inferring seniority', threshold: 68 }, + { label: 'Building your feed', threshold: 95 }, +]; + +const EXPERIENCE_LEVEL_OPTIONS = Object.entries(UserExperienceLevel).map( + ([value, label]) => ({ + value: value as keyof typeof UserExperienceLevel, + label, + }), +); + +const getExperienceLevelOptionParts = ( + label: string, +): { title: string; meta: string | null } => { + const match = label.match(/^(.*?)(?:\s*\(([^)]+)\))?$/); + if (!match) { + return { title: label, meta: null }; + } + + return { + title: match[1].trim(), + meta: match[2]?.trim() ?? null, + }; +}; + +const ONBOARDING_AI_PROMPT_KEY = 'onboarding:ai_prompt'; +const ONBOARDING_SIGNUP_CONTEXT_KEY = 'onboarding:signup_context'; +const ONBOARDING_EXTENSION_SEEN_KEY = 'onboarding:extension_seen'; + +const GITHUB_IMPORT_STEPS = [ + { label: 'Connecting account', threshold: 12 }, + { label: 'Scanning repositories', threshold: 30 }, + { label: 'Matching interests', threshold: 46 }, + { label: 'Inferring seniority', threshold: 68 }, + { label: 'Building your feed', threshold: 96 }, +]; + +const IMPORT_ANIMATION_MS = 3500; +const FINISHING_ANIMATION_MS = 1500; + +export const OnboardingV2 = (): ReactElement => { + const router = useRouter(); + const { showLogin, isLoggedIn, isAuthReady } = useAuthContext(); + const { applyThemeMode } = useSettingsContext(); + const { completeAction } = useActions(); + const { isOnboardingComplete, isOnboardingActionsReady } = + useOnboardingActions(); + const [step, setStep] = useState('hero'); + const { + mounted, + tagsReady, + feedVisible, + heroRef, + confettiParticles, + isEdgeBrowser, + extensionImages, + } = useOnboardingAnimations(step); + const [aiPrompt, setAiPrompt] = usePersistentContext( + ONBOARDING_AI_PROMPT_KEY, + '', + ); + const [extensionSeen, setExtensionSeen] = usePersistentContext( + ONBOARDING_EXTENSION_SEEN_KEY, + false, + ); + const [authDisplay, setAuthDisplay] = useState(AuthDisplay.OnboardingSignup); + const [importFlowSource, setImportFlowSource] = + useState('github'); + const [githubImportPhase, setGithubImportPhase] = + useState('idle'); + const [githubImportProgress, setGithubImportProgress] = useState(0); + const [selectedExperienceLevel, setSelectedExperienceLevel] = useState< + keyof typeof UserExperienceLevel | null + >(null); + const [githubImportBodyHeight, setGithubImportBodyHeight] = useState< + number | null + >(null); + const [githubImportExiting, setGithubImportExiting] = useState(false); + const [signupContext, setSignupContext] = usePersistentContext< + 'github' | 'ai' | null + >(ONBOARDING_SIGNUP_CONTEXT_KEY, null, ['github', 'ai']); + const pageRef = useRef(null); + const githubResumeTimeoutRef = useRef(null); + const githubImportBodyContentRef = useRef(null); + const authFormRef = useRef( + null, + ) as React.MutableRefObject; + + const popularFeedNameValue = useMemo( + () => ({ feedName: SharedFeedPage.Popular as const }), + [], + ); + + const openSignup = useCallback( + (context: 'github' | 'ai') => { + setSignupContext(context); + setStep('prompt'); + }, + [setSignupContext], + ); + + const clearGithubResumeTimeout = useCallback(() => { + if (githubResumeTimeoutRef.current === null) { + return; + } + window.clearTimeout(githubResumeTimeoutRef.current); + githubResumeTimeoutRef.current = null; + }, []); + + const startImportFlow = useCallback( + async (source: ImportFlowSource) => { + clearGithubResumeTimeout(); + + setImportFlowSource(source); + setSelectedExperienceLevel(null); + setGithubImportProgress(0); + setGithubImportPhase('running'); + setStep('importing'); + + const apiPromise = + source === 'github' + ? requestGitHubProfileTags() + : requestOnboardingProfileTags(aiPrompt); + + // Animate progress bar to 65% via CSS transition + requestAnimationFrame(() => setGithubImportProgress(65)); + + // Wait for both API response AND minimum animation time + const [apiResult] = await Promise.allSettled([ + apiPromise, + new Promise((r) => setTimeout(r, IMPORT_ANIMATION_MS)), + ]); + + if (apiResult.status === 'fulfilled') { + setAiPrompt(''); + setSignupContext(null); + } + + setGithubImportProgress(68); + setGithubImportPhase('awaitingSeniority'); + }, + [clearGithubResumeTimeout, aiPrompt, setAiPrompt, setSignupContext], + ); + + const startGithubImportFlow = useCallback(() => { + startImportFlow('github'); + }, [startImportFlow]); + + const [autoTriggerProvider, setAutoTriggerProvider] = useState< + string | undefined + >(); + + const initiateGithubAuth = useCallback(() => { + setSignupContext('github'); + setAutoTriggerProvider('github'); + setAuthDisplay(AuthDisplay.OnboardingSignup); + setStep('auth'); + }, [setSignupContext]); + + const closeGithubImportFlow = useCallback(() => { + clearGithubResumeTimeout(); + setStep('hero'); + setGithubImportExiting(false); + setSelectedExperienceLevel(null); + setGithubImportProgress(0); + setGithubImportPhase('idle'); + setImportFlowSource('github'); + }, [clearGithubResumeTimeout]); + + const startAiProcessing = useCallback(() => { + startImportFlow('ai'); + }, [startImportFlow]); + + const handleExperienceLevelSelect = useCallback( + async (level: keyof typeof UserExperienceLevel) => { + if (githubImportPhase !== 'awaitingSeniority') { + return; + } + + clearGithubResumeTimeout(); + setSelectedExperienceLevel(level); + setGithubImportProgress((prev) => Math.max(prev, 72)); + setGithubImportPhase('confirmingSeniority'); + + await gqlClient + .request(UPDATE_USER_PROFILE_MUTATION, { + data: { experienceLevel: UserExperienceLevel[level] }, + }) + .catch(() => undefined); + + completeAction(ActionType.CompletedOnboarding); + completeAction(ActionType.EditTag); + completeAction(ActionType.ContentTypes); + + router.replace({ + pathname: `${webappUrl}onboarding`, + query: { step: 'complete' }, + }); + + githubResumeTimeoutRef.current = window.setTimeout(() => { + setGithubImportPhase('finishing'); + }, 420); + }, + [clearGithubResumeTimeout, githubImportPhase, completeAction, router], + ); + + useEffect(() => { + applyThemeMode(ThemeMode.Dark); + return () => { + applyThemeMode(); + }; + }, [applyThemeMode]); + + useEffect(() => { + if ( + !isAuthReady || + ( + ['auth', 'importing', 'extension', 'complete'] as OnboardingStep[] + ).includes(step) + ) { + return; + } + + const urlStep = router.query.step as string | undefined; + + if (urlStep === 'complete') { + if (!isLoggedIn) { + router.replace(`${webappUrl}onboarding`); + return; + } + + if (extensionSeen) { + setStep('complete'); + } else { + setStep('extension'); + } + + return; + } + + const flow = router.query.flow as string | undefined; + + if (flow === 'github') { + if (!isLoggedIn) { + router.replace(`${webappUrl}onboarding`); + return; + } + startGithubImportFlow(); + return; + } + + if (isLoggedIn && signupContext === 'github') { + startGithubImportFlow(); + return; + } + + if (isLoggedIn && signupContext === 'ai' && aiPrompt) { + startAiProcessing(); + return; + } + + if (!isOnboardingActionsReady) { + return; + } + + if (!isLoggedIn) { + return; + } + + if (isOnboardingComplete) { + redirectToApp(router); + } else { + setStep('chooser'); + } + }, [ + isAuthReady, + isLoggedIn, + isOnboardingActionsReady, + isOnboardingComplete, + extensionSeen, + aiPrompt, + signupContext, + step, + router, + setSignupContext, + startGithubImportFlow, + startAiProcessing, + ]); + + useEffect(() => { + return () => { + clearGithubResumeTimeout(); + }; + }, [clearGithubResumeTimeout]); + + useEffect(() => { + if (githubImportPhase !== 'finishing') { + return undefined; + } + + setGithubImportProgress(100); + + const timers: ReturnType[] = []; + const track = (fn: () => void, delay: number) => { + const id = setTimeout(fn, delay); + timers.push(id); + return id; + }; + + track(() => { + setGithubImportPhase('complete'); + track(() => { + setGithubImportExiting(true); + track(() => { + setGithubImportExiting(false); + setStep('extension'); + }, 350); + }, 600); + }, FINISHING_ANIMATION_MS); + + return () => timers.forEach(clearTimeout); + }, [githubImportPhase]); + + const dismissExtensionPromo = useCallback(() => { + setExtensionSeen(true); + setStep('complete'); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }, [setExtensionSeen]); + const closeSignupChooser = useCallback(() => { + setStep('hero'); + }, []); + const closeAuthSignup = useCallback(() => { + setStep('hero'); + setAuthDisplay(AuthDisplay.OnboardingSignup); + }, []); + const openLogin = useCallback(() => { + showLogin({ + trigger: AuthTriggers.MainButton, + options: { isLogin: true }, + }); + }, [showLogin]); + const openSignupAuth = useCallback(() => { + setAuthDisplay(AuthDisplay.OnboardingSignup); + setStep('auth'); + }, []); + const isAiSetupContext = signupContext === 'ai'; + const canStartAiFlow = aiPrompt?.trim().length > 0; + const isAwaitingSeniorityInput = githubImportPhase === 'awaitingSeniority'; + const importSteps = useMemo( + () => + importFlowSource === 'github' ? GITHUB_IMPORT_STEPS : AI_IMPORT_STEPS, + [importFlowSource], + ); + const currentImportStep = useMemo(() => { + if (githubImportPhase === 'awaitingSeniority') { + return 'Waiting for your seniority level'; + } + if (githubImportPhase === 'confirmingSeniority') { + return 'Applying your seniority level'; + } + if (githubImportPhase === 'complete') { + return 'Your feed is ready'; + } + + const upcomingStep = importSteps.find( + (s) => githubImportProgress < s.threshold, + ); + return upcomingStep?.label ?? 'Building personalized feed'; + }, [githubImportPhase, githubImportProgress, importSteps]); + const githubImportBodyPhase = useMemo(() => { + if ( + githubImportPhase === 'running' || + githubImportPhase === 'finishing' || + githubImportPhase === 'confirmingSeniority' || + githubImportPhase === 'complete' + ) { + return 'checklist'; + } + if (githubImportPhase === 'awaitingSeniority') { + return 'seniority'; + } + + return 'default'; + }, [githubImportPhase]); + + useEffect(() => { + if (githubImportBodyPhase === 'default') { + setGithubImportBodyHeight(null); + return undefined; + } + + const contentNode = githubImportBodyContentRef.current; + if (!contentNode) { + return undefined; + } + + const updateHeight = () => { + setGithubImportBodyHeight(contentNode.getBoundingClientRect().height); + }; + + updateHeight(); + if (typeof ResizeObserver === 'undefined') { + return undefined; + } + const resizeObserver = new ResizeObserver(updateHeight); + resizeObserver.observe(contentNode); + + return () => { + resizeObserver.disconnect(); + }; + }, [githubImportBodyPhase]); + + return ( +
+ + {/* ── Dummy Header (desktop/tablet only) ── */} +
+ + {!isLoggedIn && ( +
+ + +
+ )} +
+ + {/* ── Dummy Sidebar (laptop only) ── */} + + + {/* ── Hero ── */} +
+
+ +
+ + +
+
+ {/* Dot grid — shifts subtly with scroll */} +
+ + {/* Floating particles */} +
+
+
+
+
+
+
+ {tagsReady && + RISING_TAGS_DESKTOP.map((tag) => ( + + {tag.label} + + ))} +
+ + {/* Single radial hero glow */} +
+ + {/* Centered text content */} +
+
+ {/* Mobile-only rising tags */} +
+ {tagsReady && + RISING_TAGS_MOBILE.map((tag) => ( + + {tag.label} + + ))} +
+ + {/* Headline */} +
+

+ Join top dev community. +
+ + Build your feed identity. + +

+
+ + {/* Subtext */} +
+

+ Tap into live signals from the global dev community, then lock + your feed to your stack with GitHub import or AI setup. +

+
+ + {/* Hero CTA group */} +
+
+
+ + +
+
+ + {/* Mobile-only bottom rising tags */} +
+ {tagsReady && + RISING_TAGS_MOBILE.map((tag) => ( + + {tag.label} + + ))} +
+
+
+ + {/* ── Full-screen confetti (fixed, above everything) ── */} + {step === 'complete' && ( +
+ {confettiParticles.map((p) => { + const sizeMap: Record = { + xl: 'h-4 w-2.5', + lg: 'h-3 w-2', + md: 'h-2.5 w-1.5', + }; + const sizeClass = sizeMap[p.size] ?? 'h-2 w-1'; + const shapeMap: Record = { + circle: 'rounded-full', + star: 'onb-confetti-star', + }; + const shapeClass = shapeMap[p.shape] ?? 'rounded-[1px]'; + return ( + + ); + })} +
+ )} + + {/* ── Feed Ready: Celebration Banner ── */} + {step === 'complete' && ( +
+ {/* Radial burst glows — multi-layered */} +
+
+
+ + {/* Sparkle accents */} + {[ + { left: '15%', top: '18%', delay: '200ms', size: 12 }, + { left: '80%', top: '12%', delay: '500ms', size: 16 }, + { left: '25%', top: '65%', delay: '700ms', size: 10 }, + { left: '72%', top: '55%', delay: '400ms', size: 14 }, + { left: '50%', top: '8%', delay: '100ms', size: 18 }, + { left: '90%', top: '40%', delay: '600ms', size: 8 }, + ].map((s) => ( + + + + ))} + +
+ {/* Celebration icon with glow ring */} +
+
+
+ + + + +
+
+ + {/* Headline */} +

+ Your feed is ready +

+

+ Here's how to get the most out of daily.dev +

+ + {/* Action chips */} +
+ {/* Install extension */} + + + {/* Get mobile app */} + + + {/* Enable notifications */} + +
+ + {/* Go to feed */} + +
+
+ )} + + {/* ── Feed ── */} +
+ + + + + {step !== 'complete' && ( +
+ +
+ )} +
+
+
+
+
+ + {/* ── Header signup chooser popup ── */} + {step === 'chooser' && ( +
+
+ +
+ + +
+
+

+ Stay up to date, level up with the community, and unlock more. +

+

+ Build your developer identity +

+
+ +
+ {/* ── Path A: GitHub ── */} +
+ {/* Animated orb — full-width energy field */} +
+
+
+ + + + + + + + + + {[ + { + px: '-6rem', + py: '-3.5rem', + dur: '3.0s', + delay: '0s', + color: 'bg-accent-cheese-default', + }, + { + px: '5.5rem', + py: '-4rem', + dur: '3.4s', + delay: '0.5s', + color: 'bg-accent-water-default', + }, + { + px: '-5rem', + py: '3.5rem', + dur: '3.2s', + delay: '1.0s', + color: 'bg-accent-cabbage-default', + }, + { + px: '6rem', + py: '3rem', + dur: '3.6s', + delay: '1.5s', + color: 'bg-accent-onion-default', + }, + { + px: '0.5rem', + py: '-5rem', + dur: '2.8s', + delay: '0.7s', + color: 'bg-accent-cheese-default', + }, + { + px: '-6.5rem', + py: '0.5rem', + dur: '3.1s', + delay: '1.2s', + color: 'bg-accent-water-default', + }, + ].map((p) => ( + + ))} +
+ +
+
+ +

+ One-click setup +

+

+ Connect GitHub and let our AI do the rest. +

+ +
+ {[ + { + text: 'We spot your stack from GitHub', + icon: 'stack', + }, + { + text: 'AI matches your skills to topics', + icon: 'ai', + }, + { + text: 'Your feed is ready in seconds', + icon: 'feed', + }, + ].map(({ text, icon }) => ( +
+ + {icon === 'stack' && ( + + )} + {icon === 'ai' && ( + + )} + {icon === 'feed' && ( + + )} + + + {text} + +
+ ))} +
+ +
+ +
+
+ +
+

+ Read-only access · No special permissions +

+
+ + {/* ── Path B: Manual ── */} +
+ {/* Static icon zone */} +
+
+
+ +
+
+ +

+ Tell our AI about yourself +

+

+ Describe your stack and let AI build your feed. +

+ + {/* Textarea */} +
+