From 94680083862731250efcd4d19295847d495e4d74 Mon Sep 17 00:00:00 2001 From: Mehdi Sakout Date: Sat, 28 Jan 2023 21:04:25 +0100 Subject: [PATCH 01/13] auto format and organize imports --- .vscode/settings.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index 7a73a41b..4090ed0e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,2 +1,6 @@ { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + } } \ No newline at end of file From ee286013feac4031ec8bf9e69455b0cd5e994369 Mon Sep 17 00:00:00 2001 From: Mehdi Sakout Date: Sat, 28 Jan 2023 22:00:04 +0100 Subject: [PATCH 02/13] implement the onboarding feature --- src/App.js | 26 ++- .../onboarding/components/OnboardingModal.tsx | 63 +++++++ .../onboarding/components/steps/HelloTab.tsx | 118 ++++++++++++ .../components/steps/LanguagesTab.tsx | 34 ++++ .../components/steps/SourcesTab.tsx | 32 ++++ .../onboarding/components/steps/tabs.css | 105 +++++++++++ src/features/onboarding/index.ts | 1 + src/features/onboarding/types/index.ts | 8 + src/lib/analytics.ts | 172 +++++++++++------- src/stores/preferences.ts | 17 +- 10 files changed, 498 insertions(+), 78 deletions(-) create mode 100644 src/features/onboarding/components/OnboardingModal.tsx create mode 100644 src/features/onboarding/components/steps/HelloTab.tsx create mode 100644 src/features/onboarding/components/steps/LanguagesTab.tsx create mode 100644 src/features/onboarding/components/steps/SourcesTab.tsx create mode 100644 src/features/onboarding/components/steps/tabs.css create mode 100644 src/features/onboarding/index.ts create mode 100644 src/features/onboarding/types/index.ts diff --git a/src/App.js b/src/App.js index 7f78a20c..a80f816b 100644 --- a/src/App.js +++ b/src/App.js @@ -1,16 +1,23 @@ -import { useState, useEffect } from 'react' +import React, { Suspense, useEffect, useState } from 'react' +import 'react-contexify/dist/ReactContexify.css' import 'src/assets/App.css' import { Footer, Header } from 'src/components/Layout' import { BookmarksSidebar } from 'src/features/bookmarks' import { MarketingBanner } from 'src/features/MarketingBanner' -import { ScrollCardsNavigator } from './components/Layout' -import { AppContentLayout } from './components/Layout' -import 'react-contexify/dist/ReactContexify.css' -import { setupAnalytics, trackPageView, setupIdentification } from 'src/lib/analytics' +import { setupAnalytics, setupIdentification, trackPageView } from 'src/lib/analytics' +import { useUserPreferences } from 'src/stores/preferences' +import { AppContentLayout, ScrollCardsNavigator } from './components/Layout' +import { isWebOrExtensionVersion } from './utils/Environment' + +const OnboardingModal = React.lazy(() => + import('src/features/onboarding').then((module) => ({ default: module.OnboardingModal })) +) function App() { const [showSideBar, setShowSideBar] = useState(false) const [showSettings, setShowSettings] = useState(false) + const [showOnboarding, setShowOnboarding] = useState(true) + const { onboardingCompleted } = useUserPreferences() useEffect(() => { setupAnalytics() @@ -21,7 +28,16 @@ function App() { return ( <> +
+ {!onboardingCompleted && isWebOrExtensionVersion() === 'extension' && ( + + + + )}
void +} + +export const OnboardingModal = ({ showOnboarding, setShowOnboarding }: OnboardingModalProps) => { + const { markOnboardingAsCompleted } = useUserPreferences() + + useEffect(() => { + trackOnboardingStart() + }, []) + return ( + setShowOnboarding(false)} + contentLabel="Onboarding" + className="Modal" + style={{ + overlay: { + zIndex: 3, + }, + }} + overlayClassName="Overlay"> +
+ { + trackOnboardingSkip() + markOnboardingAsCompleted(null) + setShowOnboarding(false) + }} + onFinish={(tabsData) => { + trackOnboardingFinish() + if (tabsData) { + const { icon, ...occupation } = tabsData + markOnboardingAsCompleted(occupation) + } + + setShowOnboarding(false) + }} + /> +
+
+ ) +} diff --git a/src/features/onboarding/components/steps/HelloTab.tsx b/src/features/onboarding/components/steps/HelloTab.tsx new file mode 100644 index 00000000..1c88b744 --- /dev/null +++ b/src/features/onboarding/components/steps/HelloTab.tsx @@ -0,0 +1,118 @@ +import clsx from 'clsx' +import { useState } from 'react' +import { AiFillMobile, AiFillSecurityScan } from 'react-icons/ai' +import { BsArrowRight, BsFillGearFill } from 'react-icons/bs' +import { FaDatabase, FaPaintBrush, FaRobot, FaServer } from 'react-icons/fa' +import { RiDeviceFill } from 'react-icons/ri' +import { TbDots } from 'react-icons/tb' +import { StepProps } from 'src/components/Elements' +import { Occupation } from '../../types' + +const OCCUPATIONS: Occupation[] = [ + { + title: 'Front-End Engineer', + icon: FaPaintBrush, + sources: ['devto', 'github', 'medium', 'hashnode'], + tags: ['javascript', 'typescript'], + }, + { + title: 'Back-End Engineer', + icon: BsFillGearFill, + sources: ['devto', 'github', 'medium', 'hashnode'], + tags: ['go', 'php', 'ruby', 'rust', 'r'], + }, + { + title: 'Full Stack Engineer', + icon: RiDeviceFill, + sources: ['devto', 'github', 'medium', 'hashnode'], + tags: ['javascript', 'typescript', 'php', 'ruby', 'rust'], + }, + { + title: 'Mobile', + icon: AiFillMobile, + sources: ['reddit', 'github', 'medium', 'hashnode'], + tags: ['android', 'kotlin', 'java', 'swift', 'objective-c'], + }, + { + title: 'Devops Engineer', + icon: FaServer, + sources: ['freecodecamp', 'github', 'reddit', 'devto'], + tags: ['devops', 'bash'], + }, + { + title: 'Data Engineer', + icon: FaDatabase, + sources: ['freecodecamp', 'github', 'reddit', 'devto'], + tags: ['data-science', 'python', 'artificial-intelligence', 'machine-learning'], + }, + { + title: 'Security Engineer', + icon: AiFillSecurityScan, + sources: ['freecodecamp', 'github', 'reddit', 'devto'], + tags: ['c++', 'bash', 'python'], + }, + { + title: 'ML Engineer', + icon: FaRobot, + sources: ['github', 'freecodecamp', 'hackernews', 'devto'], + tags: ['machine-learning', 'artificial-intelligence', 'python'], + }, + { + title: 'Other', + icon: TbDots, + sources: ['hackernews', 'github', 'producthunt', 'devto'], + tags: [], + }, +] + +export const HelloTab = ({ + moveToNext, + moveToPrevious, + setTabsData, + tabsData, +}: StepProps) => { + const [selectedOccupation, setSelectedOccupation] = useState( + tabsData || OCCUPATIONS[0] + ) + const onOccupationClicked = (occupation: Occupation) => { + setSelectedOccupation(occupation) + } + + const onClickNext = () => { + if (selectedOccupation === undefined) { + return + } + + setTabsData(selectedOccupation) + moveToNext && moveToNext() + } + return ( +
+
+

Hi, 👋 Welcome to Hackertab

+

Let's customize your Hackertab experience!

+
+
+ {OCCUPATIONS.map((occ, index) => { + return ( + + ) + })} +
+
+ + +
+
+ ) +} diff --git a/src/features/onboarding/components/steps/LanguagesTab.tsx b/src/features/onboarding/components/steps/LanguagesTab.tsx new file mode 100644 index 00000000..c3d6c72f --- /dev/null +++ b/src/features/onboarding/components/steps/LanguagesTab.tsx @@ -0,0 +1,34 @@ +import { ChipsSet, StepProps } from 'src/components/Elements' +import { useRemoteConfigStore } from 'src/features/remoteConfig' +import { Occupation } from '../../types' + +export const LanguagesTab = ({ moveToPrevious, moveToNext, tabsData }: StepProps) => { + const { supportedTags } = useRemoteConfigStore() + + const sources = supportedTags + .map((tag) => { + return { + label: tag.label, + value: tag.value, + } + }) + .sort((a, b) => (a.label > b.label ? 1 : -1)) + + return ( +
+
+

💻 Select your languages & topics

+

Select the languages you're interested in following.

+
+
+ +
+
+ + +
+
+ ) +} diff --git a/src/features/onboarding/components/steps/SourcesTab.tsx b/src/features/onboarding/components/steps/SourcesTab.tsx new file mode 100644 index 00000000..0018bb1c --- /dev/null +++ b/src/features/onboarding/components/steps/SourcesTab.tsx @@ -0,0 +1,32 @@ +import { BsArrowRight } from 'react-icons/bs' +import { ChipsSet, StepProps } from 'src/components/Elements' +import { SUPPORTED_CARDS } from '../../../../config' +import { Occupation } from '../../types' + +export const SourcesTab = ({ moveToPrevious, moveToNext, tabsData }: StepProps) => { + const sources = SUPPORTED_CARDS.map((source) => { + return { + label: source.label, + value: source.value, + icon: source.icon, + } + }).sort((a, b) => (a.label > b.label ? 1 : -1)) + + return ( +
+
+

📙 Pick your sources

+

Select the sources you're interested in following.

+
+
+ +
+
+ + +
+
+ ) +} diff --git a/src/features/onboarding/components/steps/tabs.css b/src/features/onboarding/components/steps/tabs.css new file mode 100644 index 00000000..278f16bf --- /dev/null +++ b/src/features/onboarding/components/steps/tabs.css @@ -0,0 +1,105 @@ +.onboardingModal { + padding: 24px 0 0 0; +} +.occupations { + display: grid; + justify-content: center; + grid-template-columns: repeat(3, 110px); + gap: 18px; + grid-auto-rows: minmax(110px, auto); + margin: 0 72px; +} + +.occupation { + background: none; + border: 1px solid var(--chip-border-color); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + row-gap: 6px; + border-radius: 8px; + padding: 0 12px; + cursor: pointer; +} +.occupation:hover { + filter: brightness(95%); +} +.occupation.active { + border: none; + background-color: var(--chip-active-background); +} +.occupation.active .occupationIcon, +.occupation.active .occupationTitle { + color: var(--chip-active-text); +} + +.occupationIcon { + width: 24px; + height: 24px; + color: var(--chip-text); +} +.occupationTitle { + margin: 0; + font-family: nunito; + font-size: 15px; + text-align: center; + color: var(--chip-text); +} +.tabHeader { + display: flex; + flex-direction: column; + justify-content: center; + gap: 10px 0; + padding: 32px 0 42px 0; +} +.tabTitle { + margin: 0; + font-family: nunito; + text-align: center; + font-weight: bold; + color: var(--tab-title-color); +} +.tabBody { + margin: 0; + text-align: center; + font-size: 18px; + color: var(--tab-description-color); +} +.tabContent { + border: 1px solid var(--tab-border-color); + padding: 16px; + border-radius: 12px; +} +.tabFooter { + display: flex; + flex-direction: row; + column-gap: 12px; + align-items: center; + margin-top: 32px; + justify-content: flex-end; +} +.tabFooter button { + border: none; + background: var(--tab-neutral-button-background); + color: var(--tab-neutral-button-text); + border-radius: 50px; + padding: 8px 18px; + cursor: pointer; + display: flex; + align-items: center; + column-gap: 8px; +} +.tabFooter button:hover { + filter: brightness(95%); +} + +.tabFooter .positiveButton { + background: var(--tab-positive-button-background); + color: var(--tab-positive-button-text); +} + +.sources { + justify-content: center; + margin: 0 64px; +} diff --git a/src/features/onboarding/index.ts b/src/features/onboarding/index.ts new file mode 100644 index 00000000..157c3a7e --- /dev/null +++ b/src/features/onboarding/index.ts @@ -0,0 +1 @@ +export * from "./components/OnboardingModal" \ No newline at end of file diff --git a/src/features/onboarding/types/index.ts b/src/features/onboarding/types/index.ts new file mode 100644 index 00000000..8d3b6bc1 --- /dev/null +++ b/src/features/onboarding/types/index.ts @@ -0,0 +1,8 @@ +import { IconType } from 'react-icons/lib' + +export type Occupation = { + title: string + icon: IconType + sources: string[] + tags: string[] +} diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts index 411a181f..85eb1a0d 100644 --- a/src/lib/analytics.ts +++ b/src/lib/analytics.ts @@ -1,9 +1,9 @@ -import AppStorage from './localStorage'; -import { init, track, identify, Identify } from '@amplitude/analytics-browser' -import { isDevelopment } from 'src/utils/Environment'; -import { ANALYTICS_SDK_KEY, ANALYTICS_ENDPOINT, LS_ANALYTICS_ID_KEY } from 'src/config' -import {getAppVersion} from "src/utils/Os"; -import { useUserPreferences } from "src/stores/preferences"; +import { identify, Identify, init, track } from '@amplitude/analytics-browser' +import { ANALYTICS_ENDPOINT, ANALYTICS_SDK_KEY, LS_ANALYTICS_ID_KEY } from 'src/config' +import { useUserPreferences } from 'src/stores/preferences' +import { isDevelopment } from 'src/utils/Environment' +import { getAppVersion } from 'src/utils/Os' +import AppStorage from './localStorage' enum Objects { PAGE = 'Page', @@ -18,6 +18,7 @@ enum Objects { LISTING_MODE = 'Listing Mode', CHANGE_LOG = 'Change Log', MARKETING_CAMPAIGN = 'Marketing Campaign', + ONBOARDING = 'Onboarding', } enum Verbs { @@ -32,6 +33,9 @@ enum Verbs { BOOKMARK = 'Bookmark', UNBOOKMARK = 'Unbookmark', REMOVE = 'Remove', + START = 'Start', + FINISH = 'Finish', + SKIP = 'Skip', } export enum Attributes { @@ -52,7 +56,8 @@ export enum Attributes { TITLE = 'Title', LINK = 'Link', SOURCE_TAGS = 'Source Tags', - CAMPAIGN_ID = 'Campaign Id' + CAMPAIGN_ID = 'Campaign Id', + OCCUPATION = 'Occupation', } const _SEP_ = ' ' @@ -61,13 +66,21 @@ export const setupAnalytics = () => { init(ANALYTICS_SDK_KEY, getRandomUserId(), { disableCookies: true, serverUrl: ANALYTICS_ENDPOINT, - appVersion: getAppVersion() || "0.0.0", - useBatch: false + appVersion: getAppVersion() || '0.0.0', + useBatch: false, }) } export const setupIdentification = () => { - const { userSelectedTags, theme, cards, listingMode, openLinksNewTab, searchEngine, } = useUserPreferences.getState(); + const { + userSelectedTags, + onboardingResult, + theme, + cards, + listingMode, + openLinksNewTab, + searchEngine, + } = useUserPreferences.getState() identifyUserProperty(Attributes.RESOLUTION, getScreenResolution()) identifyUserLanguages(userSelectedTags.map((tag: any) => tag.value)) @@ -76,13 +89,16 @@ export const setupIdentification = () => { identifyUserListingMode(listingMode) identifyUserSearchEngine(searchEngine) identifyUserLinksInNewTab(openLinksNewTab) + if (onboardingResult?.title) { + identifyUserProperty(Attributes.OCCUPATION, onboardingResult.title) + } } export const trackPageView = (pageName: string) => { trackEvent({ object: Objects.PAGE, verb: Verbs.VIEW, - attributes: { [Attributes.PAGE_NAME]: pageName } + attributes: { [Attributes.PAGE_NAME]: pageName }, }) } @@ -90,7 +106,7 @@ export const trackPageScroll = (direction: 'left' | 'right') => { trackEvent({ object: Objects.PAGE, verb: Verbs.SCROLL, - attributes: { [Attributes.DIRECTION]: direction } + attributes: { [Attributes.DIRECTION]: direction }, }) } @@ -98,7 +114,7 @@ export const trackSearchEngineUse = (searchEngineName: string) => { trackEvent({ object: Objects.SEARCH_ENGINE, verb: Verbs.SEARCH, - attributes: { [Attributes.SEARCH_ENGINE]: searchEngineName } + attributes: { [Attributes.SEARCH_ENGINE]: searchEngineName }, }) } @@ -106,15 +122,15 @@ export const trackSearchEngineSelect = (searchEngineName: string) => { trackEvent({ object: Objects.SEARCH_ENGINE, verb: Verbs.SELECT, - attributes: { [Attributes.SEARCH_ENGINE]: searchEngineName } + attributes: { [Attributes.SEARCH_ENGINE]: searchEngineName }, }) } -export const trackThemeSelect = (theme: "dark" | "light") => { +export const trackThemeSelect = (theme: 'dark' | 'light') => { trackEvent({ object: Objects.THEME, verb: Verbs.SELECT, - attributes: { [Attributes.THEME]: theme } + attributes: { [Attributes.THEME]: theme }, }) } @@ -122,7 +138,7 @@ export const trackLanguageAdd = (languageName: string) => { trackEvent({ object: Objects.LANGUAGE, verb: Verbs.ADD, - attributes: { [Attributes.LANGUAGE]: languageName } + attributes: { [Attributes.LANGUAGE]: languageName }, }) } @@ -130,7 +146,7 @@ export const trackLanguageRemove = (languageName: string) => { trackEvent({ object: Objects.LANGUAGE, verb: Verbs.REMOVE, - attributes: { [Attributes.LANGUAGE]: languageName } + attributes: { [Attributes.LANGUAGE]: languageName }, }) } @@ -138,7 +154,7 @@ export const trackSourceAdd = (sourceName: string) => { trackEvent({ object: Objects.SOURCE, verb: Verbs.ADD, - attributes: { [Attributes.SOURCE]: sourceName } + attributes: { [Attributes.SOURCE]: sourceName }, }) } @@ -146,46 +162,39 @@ export const trackSourceRemove = (sourceName: string) => { trackEvent({ object: Objects.SOURCE, verb: Verbs.REMOVE, - attributes: { [Attributes.SOURCE]: sourceName } + attributes: { [Attributes.SOURCE]: sourceName }, }) } -export const trackListingModeSelect = (listingMode: "compact" | "normal") => { +export const trackListingModeSelect = (listingMode: 'compact' | 'normal') => { trackEvent({ object: Objects.LISTING_MODE, verb: Verbs.SELECT, - attributes: { [Attributes.LISTING_MODE]: listingMode } + attributes: { [Attributes.LISTING_MODE]: listingMode }, }) } -export const trackLinkBookmark = (attributes: { - [P: string]: string; -}) => { +export const trackLinkBookmark = (attributes: { [P: string]: string }) => { trackEvent({ object: Objects.LINK, verb: Verbs.BOOKMARK, - attributes + attributes, }) } -export const trackLinkUnBookmark = (attributes: { - [P: string]: string; -}) => { +export const trackLinkUnBookmark = (attributes: { [P: string]: string }) => { trackEvent({ object: Objects.LINK, verb: Verbs.UNBOOKMARK, - attributes + attributes, }) } -export const trackLinkOpen = (attributes: { - [P: string]: string | number | undefined; -}) => { - +export const trackLinkOpen = (attributes: { [P: string]: string | number | undefined }) => { trackEvent({ object: Objects.LINK, verb: Verbs.OPEN, - attributes: attributes + attributes: attributes, }) } @@ -193,7 +202,7 @@ export const trackTabTarget = (enabled: boolean) => { trackEvent({ object: Objects.TAB, verb: Verbs.TARGET, - attributes: { [Attributes.TARGET]: enabled ? "New Tab" : "Same Tab" } + attributes: { [Attributes.TARGET]: enabled ? 'New Tab' : 'Same Tab' }, }) } @@ -201,7 +210,7 @@ export const trackCardLanguageSelect = (sourceName: string, languageName: string trackEvent({ object: Objects.CARD, verb: Verbs.SELECT, - attributes: { [Attributes.LANGUAGE]: languageName, [Attributes.SOURCE]: sourceName } + attributes: { [Attributes.LANGUAGE]: languageName, [Attributes.SOURCE]: sourceName }, }) } @@ -209,14 +218,14 @@ export const trackCardDateRangeSelect = (sourceName: string, dateRange: string) trackEvent({ object: Objects.CARD, verb: Verbs.SELECT, - attributes: { [Attributes.DATE_RANGE]: dateRange, [Attributes.SOURCE]: sourceName } + attributes: { [Attributes.DATE_RANGE]: dateRange, [Attributes.SOURCE]: sourceName }, }) } export const trackChangeLogOpen = () => { trackEvent({ object: Objects.CHANGE_LOG, - verb: Verbs.OPEN + verb: Verbs.OPEN, }) } @@ -224,7 +233,7 @@ export const trackMarketingCampaignOpen = (campaignId: string) => { trackEvent({ object: Objects.MARKETING_CAMPAIGN, verb: Verbs.OPEN, - attributes: { [Attributes.CAMPAIGN_ID]: campaignId } + attributes: { [Attributes.CAMPAIGN_ID]: campaignId }, }) } @@ -232,7 +241,7 @@ export const trackMarketingCampaignClose = (campaignId: string) => { trackEvent({ object: Objects.MARKETING_CAMPAIGN, verb: Verbs.CLOSE, - attributes: { [Attributes.CAMPAIGN_ID]: campaignId } + attributes: { [Attributes.CAMPAIGN_ID]: campaignId }, }) } @@ -240,23 +249,45 @@ export const trackMarketingCampaignView = (campaignId: string) => { trackEvent({ object: Objects.MARKETING_CAMPAIGN, verb: Verbs.VIEW, - attributes: { [Attributes.CAMPAIGN_ID]: campaignId } + attributes: { [Attributes.CAMPAIGN_ID]: campaignId }, + }) +} + +export const trackOnboardingStart = () => { + trackEvent({ + object: Objects.ONBOARDING, + verb: Verbs.START, }) } + +export const trackOnboardingSkip = () => { + trackEvent({ + object: Objects.ONBOARDING, + verb: Verbs.SKIP, + }) +} + +export const trackOnboardingFinish = () => { + trackEvent({ + object: Objects.ONBOARDING, + verb: Verbs.FINISH, + }) +} + // Identification export const identifyUserLanguages = (languages: string[]) => { identifyUserProperty(Attributes.LANGUAGES, languages) } -export const identifyUserListingMode = (listingMode: "compact" | "normal") => { +export const identifyUserListingMode = (listingMode: 'compact' | 'normal') => { identifyUserProperty(Attributes.LISTING_MODE, listingMode) } export const identifyUserCards = (sources: string[]) => { identifyUserProperty(Attributes.SOURCES, sources) } -export const identifyUserTheme = (theme: "dark" | "light") => { +export const identifyUserTheme = (theme: 'dark' | 'light') => { identifyUserProperty(Attributes.THEME, theme) } @@ -264,15 +295,15 @@ export const identifyUserSearchEngine = (searchEngineName: string) => { identifyUserProperty(Attributes.SEARCH_ENGINE, searchEngineName) } export const identifyUserLinksInNewTab = (enabled: boolean) => { - identifyUserProperty(Attributes.TARGET, enabled ? "New Tab" : "Same Tab") + identifyUserProperty(Attributes.TARGET, enabled ? 'New Tab' : 'Same Tab') } // Private functions type trackEventProps = { - object: Exclude, - verb: Exclude, + object: Exclude + verb: Exclude attributes?: { //[P in Exclude]?: string; - [P: string]: string | number | undefined; + [P: string]: string | number | undefined } } @@ -281,58 +312,60 @@ const trackEvent = ({ object, verb, attributes }: trackEventProps) => { const event = `${object}${_SEP_}${verb}` if (attributes) { - Object.keys(attributes).map(attr => { - const value = attributes[attr]; + Object.keys(attributes).map((attr) => { + const value = attributes[attr] if (!value) { - return null; + return null } - if (typeof value !== "number") { - attributes[attr] = value.toLowerCase(); + if (typeof value !== 'number') { + attributes[attr] = value.toLowerCase() } - return attr; - }); + return attr + }) // Remove http and www from links if (Object.keys(attributes).some((attr) => attr === Attributes.LINK)) { - attributes[Attributes.LINK] = (attributes[Attributes.LINK] as string).replace(/(https*:\/\/[www.]*)/, '') + attributes[Attributes.LINK] = (attributes[Attributes.LINK] as string).replace( + /(https*:\/\/[www.]*)/, + '' + ) } } if (isDevelopment()) { - console.log("analytics", event, attributes) - return; + console.log('analytics', event, attributes) + return } - track(event, attributes); + track(event, attributes) } catch (e) { - console.log("analytics", e) + console.log('analytics', e) } } const identifyUserProperty = (attributes: Attributes, value: string | string[]) => { - try { - let formatedValue; + let formatedValue if (Array.isArray(value)) { - formatedValue = value.filter(Boolean).map(item => item.toLowerCase()); + formatedValue = value.filter(Boolean).map((item) => item.toLowerCase()) } else { - formatedValue = value.toLowerCase(); + formatedValue = value.toLowerCase() } if (isDevelopment()) { - console.log("analytics", "identify", attributes, formatedValue) - return; + console.log('analytics', 'identify', attributes, formatedValue) + return } if (formatedValue == null) { - return; + return } const identity = new Identify() identity.set(attributes.toString(), formatedValue) identify(identity) } catch (e) { - console.log("analytics", e) + console.log('analytics', e) } } @@ -349,9 +382,8 @@ const getRandomUserId = () => { return userId } - const getScreenResolution = (): string => { const realWidth = window.screen.width const realHeight = window.screen.height return `${realWidth}x${realHeight}` -} \ No newline at end of file +} diff --git a/src/stores/preferences.ts b/src/stores/preferences.ts index 2b3b132d..d988c254 100644 --- a/src/stores/preferences.ts +++ b/src/stores/preferences.ts @@ -1,18 +1,21 @@ +import { Occupation } from 'src/features/onboarding/types' +import { Tag } from 'src/features/remoteConfig' import { enhanceTags } from 'src/utils/DataEnhancement' import create from 'zustand' import { persist } from 'zustand/middleware' -import { SelectedCard, Theme, ListingMode, CardSettingsType } from '../types' -import { Tag } from 'src/features/remoteConfig' +import { CardSettingsType, ListingMode, SelectedCard, Theme } from '../types' export type UserPreferencesState = { userSelectedTags: Tag[] theme: Theme openLinksNewTab: boolean + onboardingCompleted: boolean + onboardingResult: Omit | null listingMode: ListingMode searchEngine: string cards: SelectedCard[] cardsSettings: Record - firstSeenDate: number; + firstSeenDate: number } type UserPreferencesStoreActions = { @@ -24,6 +27,7 @@ type UserPreferencesStoreActions = { setCards: (selectedCards: SelectedCard[]) => void setTags: (selectedTags: Tag[]) => void setCardSettings: (card: string, settings: CardSettingsType) => void + markOnboardingAsCompleted: (occupation: Omit | null) => void } export const useUserPreferences = create( @@ -32,6 +36,8 @@ export const useUserPreferences = create( userSelectedTags: [], cardsSettings: {}, theme: 'dark', + onboardingCompleted: false, + onboardingResult: null, searchEngine: 'google', listingMode: 'normal', openLinksNewTab: true, @@ -59,6 +65,11 @@ export const useUserPreferences = create( [card]: { ...state.cardsSettings[card], ...settings }, }, })), + markOnboardingAsCompleted: (occupation: Omit | null) => + set(() => ({ + onboardingCompleted: true, + onboardingResult: occupation, + })), }), { name: 'preferences_storage', From ea9dd191a053a5dae83766b1ca125b81119f243c Mon Sep 17 00:00:00 2001 From: Mehdi Sakout Date: Sat, 28 Jan 2023 22:01:34 +0100 Subject: [PATCH 03/13] add the Steps widget component --- src/components/Elements/Steps/Steps.tsx | 126 ++++++++++++++++++++++++ src/components/Elements/Steps/index.ts | 1 + src/components/Elements/Steps/steps.css | 64 ++++++++++++ src/components/Elements/index.ts | 3 +- 4 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 src/components/Elements/Steps/Steps.tsx create mode 100644 src/components/Elements/Steps/index.ts create mode 100644 src/components/Elements/Steps/steps.css diff --git a/src/components/Elements/Steps/Steps.tsx b/src/components/Elements/Steps/Steps.tsx new file mode 100644 index 00000000..7814cdfb --- /dev/null +++ b/src/components/Elements/Steps/Steps.tsx @@ -0,0 +1,126 @@ +import { createElement, useState } from 'react' +import './steps.css' + +type StepStatus = { + [stepIndex: number]: 'current' | 'completed' | undefined | null +} + +export type Step = { + title: string + status?: 'current' | 'completed' | undefined | null + element: React.ComponentType> +} + +export type StepProps = { + showBackButton?: boolean + moveToNext?: () => void + moveToPrevious?: () => void + tabsData: T + setTabsData: (data: T) => void +} + +type StepsProps = { + steps: Array> + skipSteps?: boolean + onFinish: (tabsData: T | undefined) => void + onSkip: () => void +} + +export const Steps = ({ + onFinish, + onSkip, + skipSteps = false, + steps, +}: StepsProps) => { + const [currentStep, setCurrentStep] = useState(0) + const [tabsData, setTabsData] = useState() + const [stepsStatuses, setStepsStatuses] = useState( + steps.reduce( + (obj: StepStatus, step, index: number) => { + if (step.status) { + obj[index] = step.status + } + + return obj + }, + { 0: 'current' } + ) + ) + + const moveToNext = () => { + if (currentStep < steps.length - 1) { + setStepsStatuses((prev) => { + prev[currentStep] = 'completed' + prev[currentStep + 1] = 'current' + return prev + }) + setCurrentStep((prev) => prev + 1) + } else { + onFinish(tabsData) + } + } + + const moveToPrevious = () => { + if (currentStep > 0) { + setCurrentStep((prev) => prev - 1) + } else if (currentStep === 0) { + onSkip() + } + } + + const onStepClicked = (e: React.MouseEvent, index: number) => { + e.preventDefault() + setCurrentStep(index) + } + + const renderStep = () => { + const props = { + moveToNext, + moveToPrevious, + tabsData, + setTabsData, + } as unknown as StepProps + + const element = createElement(steps[currentStep].element, props) + + return element + } + return ( +
+ +
{renderStep()}
+
+ ) +} diff --git a/src/components/Elements/Steps/index.ts b/src/components/Elements/Steps/index.ts new file mode 100644 index 00000000..f694d2e2 --- /dev/null +++ b/src/components/Elements/Steps/index.ts @@ -0,0 +1 @@ +export * from "./Steps" \ No newline at end of file diff --git a/src/components/Elements/Steps/steps.css b/src/components/Elements/Steps/steps.css new file mode 100644 index 00000000..35e27392 --- /dev/null +++ b/src/components/Elements/Steps/steps.css @@ -0,0 +1,64 @@ +.steps { + display: flex; + align-items: center; + justify-content: center; + flex-direction: row; + list-style-type: none; + padding: 0; + margin: 0; +} +.steps .wrapper { + position: relative; +} +.step { + position: relative; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + border: none; + background: none; +} + +.step .stepBadge { + width: 38px; + height: 38px; + background-color: var(--step-normal-background-color); + border-radius: 100%; + display: flex; + justify-content: center; + align-items: center; + color: var(--step-normal-text-color); + font-size: 16px; + border: 1px solid var(--step-normal-border-color); + font-weight: bold; +} +.step .stepBadge:hover { + opacity: 0.9; + cursor: pointer; +} + +.step.active .stepBadge { + background-color: var(--step-active-background-color); + color: var(--step-active-text-color); + border: 1px solid var(--step-active-border-color); +} + +.step .stepLine { + display: flex; + flex-direction: row; + align-items: center; +} + +.step .stepLine .progressLine { + position: relative; + height: 2px; + width: 80px; + background-color: var(--step-normal-border-color); + margin-left: 12px; + margin-right: 2px; + border-radius: 50px; +} +.step.active .stepLine .progressLine { + background-color: var(--step-active-background-color); +} diff --git a/src/components/Elements/index.ts b/src/components/Elements/index.ts index d258a83a..c0a95ab6 100644 --- a/src/components/Elements/index.ts +++ b/src/components/Elements/index.ts @@ -8,4 +8,5 @@ export * from "./CardWithActions" export * from "./ClickableItem" export * from "./FloatingFilter" export * from "./InlineTextFilter" -export * from "./ChipsSet" \ No newline at end of file +export * from "./ChipsSet" +export * from "./Steps" \ No newline at end of file From ac6849f78454d907e87fd628e1cce383dda086f2 Mon Sep 17 00:00:00 2001 From: Mehdi Sakout Date: Sat, 28 Jan 2023 22:02:42 +0100 Subject: [PATCH 04/13] improve the chipset widget --- src/components/Elements/ChipsSet/ChipsSet.tsx | 30 +++++++++---- src/components/Elements/ChipsSet/chipset.css | 42 +++++++++++++++++++ .../FloatingFilter/FloatingFilter.tsx | 23 +++++----- 3 files changed, 73 insertions(+), 22 deletions(-) create mode 100644 src/components/Elements/ChipsSet/chipset.css diff --git a/src/components/Elements/ChipsSet/ChipsSet.tsx b/src/components/Elements/ChipsSet/ChipsSet.tsx index 7efe7973..6968d283 100644 --- a/src/components/Elements/ChipsSet/ChipsSet.tsx +++ b/src/components/Elements/ChipsSet/ChipsSet.tsx @@ -1,5 +1,6 @@ -import { Option } from 'src/types' import { useState } from 'react' +import { Option } from 'src/types' +import './chipset.css' type ChipProps = { option: Option @@ -10,30 +11,41 @@ type ChipProps = { const Chip = ({ option, onSelect, active = false }: ChipProps) => { return ( ) } - +type ChangeAction = 'ADD' | 'REMOVE' type ChipsSetProps = { options: Option[] - defaultValue: Option - onChange: (option: Option) => void + defaultValues?: string[] + onChange?: (action: ChangeAction, option: Option) => void } -export const ChipsSet = ({ options, onChange, defaultValue }: ChipsSetProps) => { - const [selectedChip, setSelectedChip] = useState