Skip to content

Commit ee28601

Browse files
committed
implement the onboarding feature
1 parent 9468008 commit ee28601

10 files changed

Lines changed: 498 additions & 78 deletions

File tree

src/App.js

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1-
import { useState, useEffect } from 'react'
1+
import React, { Suspense, useEffect, useState } from 'react'
2+
import 'react-contexify/dist/ReactContexify.css'
23
import 'src/assets/App.css'
34
import { Footer, Header } from 'src/components/Layout'
45
import { BookmarksSidebar } from 'src/features/bookmarks'
56
import { MarketingBanner } from 'src/features/MarketingBanner'
6-
import { ScrollCardsNavigator } from './components/Layout'
7-
import { AppContentLayout } from './components/Layout'
8-
import 'react-contexify/dist/ReactContexify.css'
9-
import { setupAnalytics, trackPageView, setupIdentification } from 'src/lib/analytics'
7+
import { setupAnalytics, setupIdentification, trackPageView } from 'src/lib/analytics'
8+
import { useUserPreferences } from 'src/stores/preferences'
9+
import { AppContentLayout, ScrollCardsNavigator } from './components/Layout'
10+
import { isWebOrExtensionVersion } from './utils/Environment'
11+
12+
const OnboardingModal = React.lazy(() =>
13+
import('src/features/onboarding').then((module) => ({ default: module.OnboardingModal }))
14+
)
1015

1116
function App() {
1217
const [showSideBar, setShowSideBar] = useState(false)
1318
const [showSettings, setShowSettings] = useState(false)
19+
const [showOnboarding, setShowOnboarding] = useState(true)
20+
const { onboardingCompleted } = useUserPreferences()
1421

1522
useEffect(() => {
1623
setupAnalytics()
@@ -21,7 +28,16 @@ function App() {
2128
return (
2229
<>
2330
<MarketingBanner />
31+
2432
<div className="App">
33+
{!onboardingCompleted && isWebOrExtensionVersion() === 'extension' && (
34+
<Suspense fallback={null}>
35+
<OnboardingModal
36+
showOnboarding={showOnboarding}
37+
setShowOnboarding={setShowOnboarding}
38+
/>
39+
</Suspense>
40+
)}
2541
<Header
2642
setShowSideBar={setShowSideBar}
2743
showSideBar={showSideBar}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { useEffect } from 'react'
2+
import ReactModal from 'react-modal'
3+
import { Steps } from 'src/components/Elements'
4+
import { trackOnboardingFinish, trackOnboardingSkip, trackOnboardingStart } from 'src/lib/analytics'
5+
import { useUserPreferences } from 'src/stores/preferences'
6+
import { HelloTab } from './steps/HelloTab'
7+
import { LanguagesTab } from './steps/LanguagesTab'
8+
import { SourcesTab } from './steps/SourcesTab'
9+
import './steps/tabs.css'
10+
11+
type OnboardingModalProps = {
12+
showOnboarding: boolean
13+
setShowOnboarding: (show: boolean) => void
14+
}
15+
16+
export const OnboardingModal = ({ showOnboarding, setShowOnboarding }: OnboardingModalProps) => {
17+
const { markOnboardingAsCompleted } = useUserPreferences()
18+
19+
useEffect(() => {
20+
trackOnboardingStart()
21+
}, [])
22+
return (
23+
<ReactModal
24+
isOpen={showOnboarding}
25+
ariaHideApp={false}
26+
shouldCloseOnEsc={false}
27+
shouldCloseOnOverlayClick={false}
28+
shouldFocusAfterRender={false}
29+
onRequestClose={() => setShowOnboarding(false)}
30+
contentLabel="Onboarding"
31+
className="Modal"
32+
style={{
33+
overlay: {
34+
zIndex: 3,
35+
},
36+
}}
37+
overlayClassName="Overlay">
38+
<div className="onboardingModal">
39+
<Steps
40+
steps={[
41+
{ title: 'Hello', element: HelloTab },
42+
{ title: 'Sources', element: SourcesTab },
43+
{ title: 'Languages', element: LanguagesTab },
44+
]}
45+
onSkip={() => {
46+
trackOnboardingSkip()
47+
markOnboardingAsCompleted(null)
48+
setShowOnboarding(false)
49+
}}
50+
onFinish={(tabsData) => {
51+
trackOnboardingFinish()
52+
if (tabsData) {
53+
const { icon, ...occupation } = tabsData
54+
markOnboardingAsCompleted(occupation)
55+
}
56+
57+
setShowOnboarding(false)
58+
}}
59+
/>
60+
</div>
61+
</ReactModal>
62+
)
63+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import clsx from 'clsx'
2+
import { useState } from 'react'
3+
import { AiFillMobile, AiFillSecurityScan } from 'react-icons/ai'
4+
import { BsArrowRight, BsFillGearFill } from 'react-icons/bs'
5+
import { FaDatabase, FaPaintBrush, FaRobot, FaServer } from 'react-icons/fa'
6+
import { RiDeviceFill } from 'react-icons/ri'
7+
import { TbDots } from 'react-icons/tb'
8+
import { StepProps } from 'src/components/Elements'
9+
import { Occupation } from '../../types'
10+
11+
const OCCUPATIONS: Occupation[] = [
12+
{
13+
title: 'Front-End Engineer',
14+
icon: FaPaintBrush,
15+
sources: ['devto', 'github', 'medium', 'hashnode'],
16+
tags: ['javascript', 'typescript'],
17+
},
18+
{
19+
title: 'Back-End Engineer',
20+
icon: BsFillGearFill,
21+
sources: ['devto', 'github', 'medium', 'hashnode'],
22+
tags: ['go', 'php', 'ruby', 'rust', 'r'],
23+
},
24+
{
25+
title: 'Full Stack Engineer',
26+
icon: RiDeviceFill,
27+
sources: ['devto', 'github', 'medium', 'hashnode'],
28+
tags: ['javascript', 'typescript', 'php', 'ruby', 'rust'],
29+
},
30+
{
31+
title: 'Mobile',
32+
icon: AiFillMobile,
33+
sources: ['reddit', 'github', 'medium', 'hashnode'],
34+
tags: ['android', 'kotlin', 'java', 'swift', 'objective-c'],
35+
},
36+
{
37+
title: 'Devops Engineer',
38+
icon: FaServer,
39+
sources: ['freecodecamp', 'github', 'reddit', 'devto'],
40+
tags: ['devops', 'bash'],
41+
},
42+
{
43+
title: 'Data Engineer',
44+
icon: FaDatabase,
45+
sources: ['freecodecamp', 'github', 'reddit', 'devto'],
46+
tags: ['data-science', 'python', 'artificial-intelligence', 'machine-learning'],
47+
},
48+
{
49+
title: 'Security Engineer',
50+
icon: AiFillSecurityScan,
51+
sources: ['freecodecamp', 'github', 'reddit', 'devto'],
52+
tags: ['c++', 'bash', 'python'],
53+
},
54+
{
55+
title: 'ML Engineer',
56+
icon: FaRobot,
57+
sources: ['github', 'freecodecamp', 'hackernews', 'devto'],
58+
tags: ['machine-learning', 'artificial-intelligence', 'python'],
59+
},
60+
{
61+
title: 'Other',
62+
icon: TbDots,
63+
sources: ['hackernews', 'github', 'producthunt', 'devto'],
64+
tags: [],
65+
},
66+
]
67+
68+
export const HelloTab = ({
69+
moveToNext,
70+
moveToPrevious,
71+
setTabsData,
72+
tabsData,
73+
}: StepProps<Occupation>) => {
74+
const [selectedOccupation, setSelectedOccupation] = useState<Occupation | undefined>(
75+
tabsData || OCCUPATIONS[0]
76+
)
77+
const onOccupationClicked = (occupation: Occupation) => {
78+
setSelectedOccupation(occupation)
79+
}
80+
81+
const onClickNext = () => {
82+
if (selectedOccupation === undefined) {
83+
return
84+
}
85+
86+
setTabsData(selectedOccupation)
87+
moveToNext && moveToNext()
88+
}
89+
return (
90+
<div>
91+
<div className="tabHeader">
92+
<h1 className="tabTitle">Hi, 👋 Welcome to Hackertab</h1>
93+
<p className="tabBody">Let's customize your Hackertab experience!</p>
94+
</div>
95+
<div className="occupations">
96+
{OCCUPATIONS.map((occ, index) => {
97+
return (
98+
<button
99+
key={occ.title}
100+
onClick={() => onOccupationClicked(occ)}
101+
className={clsx('occupation', selectedOccupation?.title === occ.title && 'active')}>
102+
<span>
103+
<occ.icon className="occupationIcon" />
104+
</span>
105+
<h3 className="occupationTitle">{occ.title}</h3>
106+
</button>
107+
)
108+
})}
109+
</div>
110+
<div className="tabFooter">
111+
<button onClick={() => moveToPrevious && moveToPrevious()}>Skip</button>
112+
<button className="positiveButton" onClick={() => onClickNext()}>
113+
<BsArrowRight /> Next
114+
</button>
115+
</div>
116+
</div>
117+
)
118+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { ChipsSet, StepProps } from 'src/components/Elements'
2+
import { useRemoteConfigStore } from 'src/features/remoteConfig'
3+
import { Occupation } from '../../types'
4+
5+
export const LanguagesTab = ({ moveToPrevious, moveToNext, tabsData }: StepProps<Occupation>) => {
6+
const { supportedTags } = useRemoteConfigStore()
7+
8+
const sources = supportedTags
9+
.map((tag) => {
10+
return {
11+
label: tag.label,
12+
value: tag.value,
13+
}
14+
})
15+
.sort((a, b) => (a.label > b.label ? 1 : -1))
16+
17+
return (
18+
<div>
19+
<div className="tabHeader">
20+
<h1 className="tabTitle">💻 Select your languages & topics</h1>
21+
<p className="tabBody">Select the languages you're interested in following.</p>
22+
</div>
23+
<div className="tabContent sources">
24+
<ChipsSet options={sources} defaultValues={tabsData.tags} />
25+
</div>
26+
<div className="tabFooter">
27+
<button onClick={() => moveToPrevious && moveToPrevious()}>Back</button>
28+
<button className="positiveButton" onClick={() => moveToNext && moveToNext()}>
29+
Finish
30+
</button>
31+
</div>
32+
</div>
33+
)
34+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { BsArrowRight } from 'react-icons/bs'
2+
import { ChipsSet, StepProps } from 'src/components/Elements'
3+
import { SUPPORTED_CARDS } from '../../../../config'
4+
import { Occupation } from '../../types'
5+
6+
export const SourcesTab = ({ moveToPrevious, moveToNext, tabsData }: StepProps<Occupation>) => {
7+
const sources = SUPPORTED_CARDS.map((source) => {
8+
return {
9+
label: source.label,
10+
value: source.value,
11+
icon: source.icon,
12+
}
13+
}).sort((a, b) => (a.label > b.label ? 1 : -1))
14+
15+
return (
16+
<div>
17+
<div className="tabHeader">
18+
<h1 className="tabTitle">📙 Pick your sources</h1>
19+
<p className="tabBody">Select the sources you're interested in following.</p>
20+
</div>
21+
<div className="tabContent sources">
22+
<ChipsSet options={sources} defaultValues={tabsData.sources} />
23+
</div>
24+
<div className="tabFooter">
25+
<button onClick={() => moveToPrevious && moveToPrevious()}>Back</button>
26+
<button className="positiveButton" onClick={() => moveToNext && moveToNext()}>
27+
<BsArrowRight /> Next
28+
</button>
29+
</div>
30+
</div>
31+
)
32+
}

0 commit comments

Comments
 (0)