diff --git a/src/components/Navigation/AccountDropdown.tsx b/src/components/Navigation/AccountDropdown.tsx index 07119ea99..7e24b0587 100644 --- a/src/components/Navigation/AccountDropdown.tsx +++ b/src/components/Navigation/AccountDropdown.tsx @@ -10,6 +10,7 @@ import { httpGet } from '../../lib/http.ts'; import { useToast } from '../../hooks/use-toast.ts'; import type { UserDocument } from '../../api/user.ts'; import { NotificationIndicator } from './NotificationIndicator.tsx'; +import { OnboardingNudge } from '../OnboardingNudge.tsx'; export type OnboardingConfig = Pick< UserDocument, @@ -24,7 +25,7 @@ export function AccountDropdown() { const [isTeamsOpen, setIsTeamsOpen] = useState(false); const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false); - const [isConfigLoading, setIsConfigLoading] = useState(true); + const [isConfigLoading, setIsConfigLoading] = useState(false); const [isOnboardingModalOpen, setIsOnboardingModalOpen] = useState(false); const [onboardingConfig, setOnboardingConfig] = useState< OnboardingConfig | undefined @@ -93,66 +94,80 @@ export function AccountDropdown() { ).length; return ( -
- {isOnboardingModalOpen && onboardingConfig && ( - { - setIsOnboardingModalOpen(false); - }} - onIgnoreTask={(taskId, status) => { - loadOnboardingConfig().finally(() => {}); - }} - /> - )} - {isCreatingRoadmap && ( - { - setIsCreatingRoadmap(false); + <> + {shouldShowOnboardingStatus && !isOnboardingModalOpen && ( + { + loadOnboardingConfig().then(() => { + setIsOnboardingModalOpen(true); + }); }} /> )} - - - {showDropdown && ( -
+ {isOnboardingModalOpen && onboardingConfig && ( + { + setIsOnboardingModalOpen(false); + }} + onIgnoreTask={(taskId, status) => { + loadOnboardingConfig().finally(() => {}); + }} + /> + )} + {isCreatingRoadmap && ( + { + setIsCreatingRoadmap(false); + }} + /> + )} + +
- )} -
+ + + {showDropdown && ( +
+ {isTeamsOpen ? ( + + ) : ( + { + setIsCreatingRoadmap(true); + setShowDropdown(false); + }} + setIsTeamsOpen={setIsTeamsOpen} + onOnboardingClick={() => { + setIsOnboardingModalOpen(true); + setShowDropdown(false); + }} + shouldShowOnboardingStatus={shouldShowOnboardingStatus} + isConfigLoading={isConfigLoading} + onboardingConfigCount={onboardingCount} + doneConfigCount={onboardingDoneCount} + /> + )} +
+ )} + + ); } diff --git a/src/components/Navigation/AccountDropdownList.tsx b/src/components/Navigation/AccountDropdownList.tsx index 79deae41d..8b3dbe1c9 100644 --- a/src/components/Navigation/AccountDropdownList.tsx +++ b/src/components/Navigation/AccountDropdownList.tsx @@ -45,7 +45,7 @@ export function AccountDropdownList(props: AccountDropdownListProps) { className={cn( 'flex h-9 w-full items-center rounded py-1 pl-3 pr-2 text-sm font-medium text-slate-100 hover:opacity-80', isConfigLoading - ? 'striped-loader-lighter flex border-slate-800 opacity-70' + ? 'striped-loader-darker flex border-slate-800 opacity-70' : 'border-slate-600 bg-slate-700', )} onClick={onOnboardingClick} diff --git a/src/components/OnboardingNudge.tsx b/src/components/OnboardingNudge.tsx new file mode 100644 index 000000000..bb2710a82 --- /dev/null +++ b/src/components/OnboardingNudge.tsx @@ -0,0 +1,69 @@ +import { cn } from '../lib/classname.ts'; +import { memo, useEffect, useState } from 'react'; +import { useScrollPosition } from '../hooks/use-scroll-position.ts'; +import { X } from 'lucide-react'; + +type OnboardingNudgeProps = { + onStartOnboarding: () => void; +}; + +const NUDGE_ONBOARDING_KEY = 'should_nudge_onboarding'; + +export function OnboardingNudge(props: OnboardingNudgeProps) { + const { onStartOnboarding } = props; + + const [isLoading, setIsLoading] = useState(false); + + const { y: scrollY } = useScrollPosition(); + + useEffect(() => { + if (localStorage.getItem(NUDGE_ONBOARDING_KEY) === null) { + localStorage.setItem(NUDGE_ONBOARDING_KEY, 'true'); + } + }, []); + + if (localStorage.getItem(NUDGE_ONBOARDING_KEY) !== 'true') { + return null; + } + + if (scrollY < 100) { + return null; + } + + return ( +
+

+ Welcome! Please take a moment to{' '} + + +

+
+ ); +} diff --git a/src/hooks/use-scroll-position.ts b/src/hooks/use-scroll-position.ts new file mode 100644 index 000000000..861e28c3c --- /dev/null +++ b/src/hooks/use-scroll-position.ts @@ -0,0 +1,22 @@ +import { useEffect, useState } from 'react'; + +export function useScrollPosition() { + const [scrollPosition, setScrollPosition] = useState<{ + x: number; + y: number; + }>({ + x: 0, + y: 0, + }); + + useEffect(() => { + const handleScroll = () => { + setScrollPosition({ x: window.scrollX, y: window.scrollY }); + }; + + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + return scrollPosition; +} diff --git a/src/styles/global.css b/src/styles/global.css index 7f84672c5..7f5977e73 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -84,7 +84,7 @@ a > code:before { animation: barberpole 15s linear infinite; } -.striped-loader-lighter { +.striped-loader-darker { background-image: repeating-linear-gradient( -45deg, transparent,