Add progress nudge on roadmap

pull/5740/head
Kamran Ahmed 6 months ago
parent 9895956531
commit 6728010173
  1. 127
      src/components/Navigation/AccountDropdown.tsx
  2. 2
      src/components/Navigation/AccountDropdownList.tsx
  3. 69
      src/components/OnboardingNudge.tsx
  4. 22
      src/hooks/use-scroll-position.ts
  5. 2
      src/styles/global.css

@ -10,6 +10,7 @@ import { httpGet } from '../../lib/http.ts';
import { useToast } from '../../hooks/use-toast.ts'; import { useToast } from '../../hooks/use-toast.ts';
import type { UserDocument } from '../../api/user.ts'; import type { UserDocument } from '../../api/user.ts';
import { NotificationIndicator } from './NotificationIndicator.tsx'; import { NotificationIndicator } from './NotificationIndicator.tsx';
import { OnboardingNudge } from '../OnboardingNudge.tsx';
export type OnboardingConfig = Pick< export type OnboardingConfig = Pick<
UserDocument, UserDocument,
@ -24,7 +25,7 @@ export function AccountDropdown() {
const [isTeamsOpen, setIsTeamsOpen] = useState(false); const [isTeamsOpen, setIsTeamsOpen] = useState(false);
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false); const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false);
const [isConfigLoading, setIsConfigLoading] = useState(true); const [isConfigLoading, setIsConfigLoading] = useState(false);
const [isOnboardingModalOpen, setIsOnboardingModalOpen] = useState(false); const [isOnboardingModalOpen, setIsOnboardingModalOpen] = useState(false);
const [onboardingConfig, setOnboardingConfig] = useState< const [onboardingConfig, setOnboardingConfig] = useState<
OnboardingConfig | undefined OnboardingConfig | undefined
@ -93,66 +94,80 @@ export function AccountDropdown() {
).length; ).length;
return ( return (
<div className="relative z-[90] animate-fade-in"> <>
{isOnboardingModalOpen && onboardingConfig && ( {shouldShowOnboardingStatus && !isOnboardingModalOpen && (
<OnboardingModal <OnboardingNudge
onboardingConfig={onboardingConfig} onStartOnboarding={() => {
onClose={() => { loadOnboardingConfig().then(() => {
setIsOnboardingModalOpen(false); setIsOnboardingModalOpen(true);
}} });
onIgnoreTask={(taskId, status) => {
loadOnboardingConfig().finally(() => {});
}}
/>
)}
{isCreatingRoadmap && (
<CreateRoadmapModal
onClose={() => {
setIsCreatingRoadmap(false);
}} }}
/> />
)} )}
<button <div className="relative z-[90] animate-fade-in">
className="relative flex h-8 w-40 items-center justify-center gap-1.5 rounded-full bg-gradient-to-r from-purple-500 to-purple-700 px-4 py-2 text-sm font-medium text-white hover:from-purple-500 hover:to-purple-600" {isOnboardingModalOpen && onboardingConfig && (
onClick={() => { <OnboardingModal
setIsTeamsOpen(false); onboardingConfig={onboardingConfig}
setShowDropdown(!showDropdown); onClose={() => {
}} setIsOnboardingModalOpen(false);
> }}
<span className="inline-flex items-center"> onIgnoreTask={(taskId, status) => {
Account&nbsp;<span className="text-gray-300">/</span>&nbsp;Teams loadOnboardingConfig().finally(() => {});
</span> }}
<ChevronDown className="h-4 w-4 shrink-0 stroke-[2.5px]" /> />
{shouldShowOnboardingStatus && !showDropdown && <NotificationIndicator />} )}
</button> {isCreatingRoadmap && (
<CreateRoadmapModal
{showDropdown && ( onClose={() => {
<div setIsCreatingRoadmap(false);
ref={dropdownRef} }}
className="absolute right-0 z-50 mt-2 min-h-[152px] w-48 rounded-md bg-slate-800 py-1 shadow-xl" />
)}
<button
className="relative flex h-8 w-40 items-center justify-center gap-1.5 rounded-full bg-gradient-to-r from-purple-500 to-purple-700 px-4 py-2 text-sm font-medium text-white hover:from-purple-500 hover:to-purple-600"
onClick={() => {
setIsTeamsOpen(false);
setShowDropdown(!showDropdown);
}}
> >
{isTeamsOpen ? ( <span className="inline-flex items-center">
<DropdownTeamList setIsTeamsOpen={setIsTeamsOpen} /> Account&nbsp;<span className="text-gray-300">/</span>&nbsp;Teams
) : ( </span>
<AccountDropdownList <ChevronDown className="h-4 w-4 shrink-0 stroke-[2.5px]" />
onCreateRoadmap={() => { {shouldShowOnboardingStatus && !showDropdown && (
setIsCreatingRoadmap(true); <NotificationIndicator />
setShowDropdown(false);
}}
setIsTeamsOpen={setIsTeamsOpen}
onOnboardingClick={() => {
setIsOnboardingModalOpen(true);
setShowDropdown(false);
}}
shouldShowOnboardingStatus={shouldShowOnboardingStatus}
isConfigLoading={isConfigLoading}
onboardingConfigCount={onboardingCount}
doneConfigCount={onboardingDoneCount}
/>
)} )}
</div> </button>
)}
</div> {showDropdown && (
<div
ref={dropdownRef}
className="absolute right-0 z-50 mt-2 min-h-[152px] w-48 rounded-md bg-slate-800 py-1 shadow-xl"
>
{isTeamsOpen ? (
<DropdownTeamList setIsTeamsOpen={setIsTeamsOpen} />
) : (
<AccountDropdownList
onCreateRoadmap={() => {
setIsCreatingRoadmap(true);
setShowDropdown(false);
}}
setIsTeamsOpen={setIsTeamsOpen}
onOnboardingClick={() => {
setIsOnboardingModalOpen(true);
setShowDropdown(false);
}}
shouldShowOnboardingStatus={shouldShowOnboardingStatus}
isConfigLoading={isConfigLoading}
onboardingConfigCount={onboardingCount}
doneConfigCount={onboardingDoneCount}
/>
)}
</div>
)}
</div>
</>
); );
} }

@ -45,7 +45,7 @@ export function AccountDropdownList(props: AccountDropdownListProps) {
className={cn( 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', '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 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', : 'border-slate-600 bg-slate-700',
)} )}
onClick={onOnboardingClick} onClick={onOnboardingClick}

@ -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 (
<div
className={cn(
'fixed left-0 right-0 top-0 z-[91] flex w-full items-center justify-center bg-yellow-300 py-1.5',
{
'striped-loader': isLoading,
},
)}
>
<p className="text-base font-semibold text-yellow-950">
Welcome! Please take a moment to{' '}
<button
type="button"
onClick={() => {
setIsLoading(true);
localStorage.setItem(NUDGE_ONBOARDING_KEY, 'false');
onStartOnboarding();
}}
className="underline"
>
complete onboarding
</button>
<button
type="button"
className="relative top-[3px] ml-1 px-1 py-1 text-yellow-600 hover:text-yellow-950"
onClick={(e) => {
e.stopPropagation();
localStorage.setItem(NUDGE_ONBOARDING_KEY, 'false');
setIsLoading(true);
}}
>
<X className="h-4 w-4" strokeWidth={3} />
</button>
</p>
</div>
);
}

@ -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;
}

@ -84,7 +84,7 @@ a > code:before {
animation: barberpole 15s linear infinite; animation: barberpole 15s linear infinite;
} }
.striped-loader-lighter { .striped-loader-darker {
background-image: repeating-linear-gradient( background-image: repeating-linear-gradient(
-45deg, -45deg,
transparent, transparent,

Loading…
Cancel
Save