From 422e8f8357c0d4e6a290555c6b24e3360bfb951e Mon Sep 17 00:00:00 2001 From: Raquel Smith Date: Wed, 15 Nov 2023 11:11:56 -0800 Subject: [PATCH] feat(onboarding): project switcher and mobile friendly (#18634) * show project switcher in top bar * move project switcher actions to topbarlogic so it's not dependent on having ingestionlogic around * set logic to make mobile friendly * only show continue button on instructions * if click back on instructions, show sdk options * make other products screen mobile friendly * use useWindowSize hook in sdks screen * fix width * make billinghero mobile friendly * make billing plan comparison mobile friendly * fix types --- .../src/layout/navigation/TopBar/TopBar.tsx | 30 ++++++++- .../layout/navigation/TopBar/topBarLogic.ts | 20 ++++++ frontend/src/scenes/billing/BillingHero.scss | 2 +- frontend/src/scenes/billing/BillingHero.tsx | 13 ++-- .../src/scenes/billing/PlanComparison.scss | 4 ++ .../src/scenes/billing/PlanComparison.tsx | 26 +++++--- .../src/scenes/ingestion/ingestionLogic.ts | 2 +- .../onboarding/OnboardingBillingStep.tsx | 2 +- .../OnboardingOtherProductsStep.tsx | 31 ++++------ .../src/scenes/onboarding/OnboardingStep.tsx | 12 +++- frontend/src/scenes/onboarding/sdks/SDKs.tsx | 34 ++++++++-- .../src/scenes/onboarding/sdks/sdksLogic.tsx | 26 +++++++- frontend/src/scenes/products/Products.tsx | 62 ++++++++++++++----- 13 files changed, 207 insertions(+), 57 deletions(-) create mode 100644 frontend/src/layout/navigation/TopBar/topBarLogic.ts diff --git a/frontend/src/layout/navigation/TopBar/TopBar.tsx b/frontend/src/layout/navigation/TopBar/TopBar.tsx index 585cb5554b1c0..9c307caa1c6f7 100644 --- a/frontend/src/layout/navigation/TopBar/TopBar.tsx +++ b/frontend/src/layout/navigation/TopBar/TopBar.tsx @@ -16,12 +16,19 @@ import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { FEATURE_FLAGS } from 'lib/constants' import { NotebookButton } from '~/layout/navigation/TopBar/NotebookButton' import { ActivationSidebarToggle } from 'lib/components/ActivationSidebar/ActivationSidebarToggle' +import { organizationLogic } from 'scenes/organizationLogic' +import { LemonButtonWithDropdown, Lettermark } from '@posthog/lemon-ui' +import { ProjectSwitcherOverlay } from '../ProjectSwitcher' +import { topBarLogic } from './topBarLogic' export function TopBar(): JSX.Element { const { isSideBarShown, noSidebar, minimalTopBar, mobileLayout } = useValues(navigationLogic) const { toggleSideBarBase, toggleSideBarMobile } = useActions(navigationLogic) const { groupNamesTaxonomicTypes } = useValues(groupsModel) const { featureFlags } = useValues(featureFlagLogic) + const { currentOrganization } = useValues(organizationLogic) + const { isProjectSwitcherShown } = useValues(topBarLogic) + const { toggleProjectSwitcher, hideProjectSwitcher } = useActions(topBarLogic) const hasNotebooks = !!featureFlags[FEATURE_FLAGS.NOTEBOOKS] @@ -71,11 +78,32 @@ export function TopBar(): JSX.Element { )}
- {!minimalTopBar && ( + {!minimalTopBar ? ( <> {hasNotebooks && } + ) : ( + currentOrganization?.teams && + currentOrganization.teams.length > 1 && ( +
+ } + onClick={() => toggleProjectSwitcher()} + dropdown={{ + visible: isProjectSwitcherShown, + onClickOutside: hideProjectSwitcher, + overlay: , + actionable: true, + placement: 'top-end', + }} + type="secondary" + fullWidth + > + Switch project + +
+ ) )} diff --git a/frontend/src/layout/navigation/TopBar/topBarLogic.ts b/frontend/src/layout/navigation/TopBar/topBarLogic.ts new file mode 100644 index 0000000000000..3ef8f08138acd --- /dev/null +++ b/frontend/src/layout/navigation/TopBar/topBarLogic.ts @@ -0,0 +1,20 @@ +import { actions, kea, reducers, path } from 'kea' + +import type { topBarLogicType } from './topBarLogicType' + +export const topBarLogic = kea([ + path(['layout', 'navigation', 'TopBar', 'topBarLogic']), + actions({ + toggleProjectSwitcher: true, + hideProjectSwitcher: true, + }), + reducers({ + isProjectSwitcherShown: [ + false, + { + toggleProjectSwitcher: (state) => !state, + hideProjectSwitcher: () => false, + }, + ], + }), +]) diff --git a/frontend/src/scenes/billing/BillingHero.scss b/frontend/src/scenes/billing/BillingHero.scss index 0928d8d935c0c..3c30a1ff02d79 100644 --- a/frontend/src/scenes/billing/BillingHero.scss +++ b/frontend/src/scenes/billing/BillingHero.scss @@ -18,5 +18,5 @@ .BillingHero__hog__img { height: 200px; width: 200px; - margin: -20px -30px; + margin: -20px 0; } diff --git a/frontend/src/scenes/billing/BillingHero.tsx b/frontend/src/scenes/billing/BillingHero.tsx index 0f74dc168071a..3dbe62ca6dc62 100644 --- a/frontend/src/scenes/billing/BillingHero.tsx +++ b/frontend/src/scenes/billing/BillingHero.tsx @@ -1,9 +1,12 @@ import { BlushingHog } from 'lib/components/hedgehogs' import './BillingHero.scss' +import useResizeObserver from 'use-resize-observer' export const BillingHero = (): JSX.Element => { + const { width, ref: billingHeroRef } = useResizeObserver() + return ( -
+

How pricing works

Get the whole hog.

@@ -13,9 +16,11 @@ export const BillingHero = (): JSX.Element => { limits as low as $0 to control spend.

-
- -
+ {width && width > 500 && ( +
+ +
+ )}
) } diff --git a/frontend/src/scenes/billing/PlanComparison.scss b/frontend/src/scenes/billing/PlanComparison.scss index 57c4ed6bd6d63..98d091b0b80d7 100644 --- a/frontend/src/scenes/billing/PlanComparison.scss +++ b/frontend/src/scenes/billing/PlanComparison.scss @@ -29,6 +29,10 @@ table.PlanComparison { &.PlanTable__th__feature { padding: 0.75rem 1.25rem 0.75rem 3.25rem; font-weight: 600; + + &.PlanTable__th__feature--reduced_padding { + padding: 0.75rem 1.25rem; + } } &.PlanTable__th__last-feature { diff --git a/frontend/src/scenes/billing/PlanComparison.tsx b/frontend/src/scenes/billing/PlanComparison.tsx index 0fcd7bbf695ca..a2e75da9e5115 100644 --- a/frontend/src/scenes/billing/PlanComparison.tsx +++ b/frontend/src/scenes/billing/PlanComparison.tsx @@ -9,6 +9,7 @@ import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { convertLargeNumberToWords, getUpgradeProductLink } from './billing-utils' import { billingLogic } from './billingLogic' import { getProductIcon } from 'scenes/products/Products' +import useResizeObserver from 'use-resize-observer' export function PlanIcon({ feature, @@ -27,7 +28,7 @@ export function PlanIcon({ ) : feature.limit ? ( <> - + {feature.limit && `${convertLargeNumberToWords(feature.limit, null)} ${feature.unit && feature.unit}${ timeDenominator ? `/${timeDenominator}` : '' @@ -36,7 +37,7 @@ export function PlanIcon({ ) : ( <> - + {feature.note} )} @@ -48,6 +49,7 @@ const getProductTiers = ( plan: BillingV2PlanType, product: BillingProductV2Type | BillingProductV2AddonType ): JSX.Element => { + const { width, ref: tiersRef } = useResizeObserver() const tiers = plan?.tiers const allTierPrices = tiers?.map((tier) => parseFloat(tier.unit_amount_usd)) @@ -59,7 +61,8 @@ const getProductTiers = ( tiers?.map((tier, i) => (
{convertLargeNumberToWords(tier.up_to, tiers[i - 1]?.up_to, true, product.unit)} @@ -72,7 +75,11 @@ const getProductTiers = (
)) ) : product?.free_allocation ? ( -
+
Up to {convertLargeNumberToWords(product?.free_allocation, null)} {product?.unit}s/mo @@ -97,6 +104,7 @@ export const PlanComparison = ({ const fullyFeaturedPlan = plans[plans.length - 1] const { reportBillingUpgradeClicked } = useActions(eventUsageLogic) const { redirectPath, billing } = useValues(billingLogic) + const { width, ref: planComparisonRef } = useResizeObserver() const upgradeButtons = plans?.map((plan) => { return ( @@ -132,7 +140,7 @@ export const PlanComparison = ({ }) return ( - +
@@ -283,6 +291,10 @@ export const PlanComparison = ({
@@ -231,8 +239,8 @@ export const PlanComparison = ({ > {feature.name}
([ }, ], isDemoProject: [ - teamLogic.values.currentTeam?.is_demo as null | boolean, + false as null | boolean, { setState: (_, { isDemoProject }) => isDemoProject, }, diff --git a/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx b/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx index c6901e9d7b829..552a490688887 100644 --- a/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx +++ b/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx @@ -43,7 +43,7 @@ export const OnboardingBillingStep = ({ reportBillingUpgradeClicked(product.type) }} > - Upgrade to paid + Subscribe ) } diff --git a/frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx b/frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx index 54618e56a0d64..6c3e92e9af68c 100644 --- a/frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx +++ b/frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx @@ -1,8 +1,8 @@ -import { LemonButton, LemonCard } from '@posthog/lemon-ui' +import { useWindowSize } from 'lib/hooks/useWindowSize' import { OnboardingStep } from './OnboardingStep' import { OnboardingStepKey, onboardingLogic } from './onboardingLogic' import { useActions, useValues } from 'kea' -import { getProductIcon } from 'scenes/products/Products' +import { ProductCard } from 'scenes/products/Products' export const OnboardingOtherProductsStep = ({ stepKey = OnboardingStepKey.OTHER_PRODUCTS, @@ -11,6 +11,8 @@ export const OnboardingOtherProductsStep = ({ }): JSX.Element => { const { product, suggestedProducts } = useValues(onboardingLogic) const { completeOnboarding } = useActions(onboardingLogic) + const { width } = useWindowSize() + const horizontalCard = width && width >= 640 return ( } stepKey={stepKey} > -
+
{suggestedProducts?.map((suggestedProduct) => ( - -
-
{getProductIcon(suggestedProduct.icon_key, 'text-2xl')}
-
-

{suggestedProduct.name}

-

{suggestedProduct.description}

-
-
-
- completeOnboarding(suggestedProduct.type)}> - Get started - -
-
+ getStartedActionOverride={() => completeOnboarding(suggestedProduct.type)} + orientation={horizontalCard ? 'horizontal' : 'vertical'} + className="w-full" + /> ))}
diff --git a/frontend/src/scenes/onboarding/OnboardingStep.tsx b/frontend/src/scenes/onboarding/OnboardingStep.tsx index a4bfdd92cbf42..171c607bf3d43 100644 --- a/frontend/src/scenes/onboarding/OnboardingStep.tsx +++ b/frontend/src/scenes/onboarding/OnboardingStep.tsx @@ -14,6 +14,7 @@ export const OnboardingStep = ({ showSkip = false, onSkip, continueOverride, + backActionOverride, }: { stepKey: OnboardingStepKey title: string @@ -22,6 +23,7 @@ export const OnboardingStep = ({ showSkip?: boolean onSkip?: () => void continueOverride?: JSX.Element + backActionOverride?: () => void }): JSX.Element => { const { hasNextStep, hasPreviousStep } = useValues(onboardingLogic) const { completeOnboarding, goToNextStep, goToPreviousStep } = useActions(onboardingLogic) @@ -39,14 +41,20 @@ export const OnboardingStep = ({
} - onClick={() => (hasPreviousStep ? goToPreviousStep() : router.actions.push(urls.products()))} + onClick={() => + backActionOverride + ? backActionOverride() + : hasPreviousStep + ? goToPreviousStep() + : router.actions.push(urls.products()) + } > Back
} > -
+

{title}

{subtitle}

{children} diff --git a/frontend/src/scenes/onboarding/sdks/SDKs.tsx b/frontend/src/scenes/onboarding/sdks/SDKs.tsx index 9a91ad06268d6..94556254fab58 100644 --- a/frontend/src/scenes/onboarding/sdks/SDKs.tsx +++ b/frontend/src/scenes/onboarding/sdks/SDKs.tsx @@ -8,6 +8,8 @@ import { useEffect } from 'react' import React from 'react' import { SDKInstructionsMap } from '~/types' import { InviteMembersButton } from '~/layout/navigation/TopBar/SitePopover' +import { IconArrowLeft } from '@posthog/icons' +import { useWindowSize } from 'lib/hooks/useWindowSize' export function SDKs({ usersAction, @@ -20,23 +22,37 @@ export function SDKs({ subtitle?: string stepKey?: OnboardingStepKey }): JSX.Element { - const { setSourceFilter, setSelectedSDK, setAvailableSDKInstructionsMap } = useActions(sdksLogic) - const { sourceFilter, sdks, selectedSDK, sourceOptions, showSourceOptionsSelect } = useValues(sdksLogic) + const { setSourceFilter, setSelectedSDK, setAvailableSDKInstructionsMap, setShowSideBySide, setPanel } = + useActions(sdksLogic) + const { sourceFilter, sdks, selectedSDK, sourceOptions, showSourceOptionsSelect, showSideBySide, panel } = + useValues(sdksLogic) const { productKey } = useValues(onboardingLogic) + const { width } = useWindowSize() + const minimumSideBySideSize = 768 useEffect(() => { setAvailableSDKInstructionsMap(sdkInstructionMap) }, []) + useEffect(() => { + width && setShowSideBySide(width > minimumSideBySideSize) + }, [width]) + return ( : undefined} + backActionOverride={!showSideBySide && panel === 'instructions' ? () => setPanel('options') : undefined} >
-
+
{showSourceOptionsSelect && (
{selectedSDK && productKey && !!sdkInstructionMap[selectedSDK.key] && ( -
+
+ {!showSideBySide && ( + } + onClick={() => setPanel('options')} + className="mb-8" + type="secondary" + > + View all SDKs + + )}
)} diff --git a/frontend/src/scenes/onboarding/sdks/sdksLogic.tsx b/frontend/src/scenes/onboarding/sdks/sdksLogic.tsx index 83203a1ef7bc4..2cfef0711bf10 100644 --- a/frontend/src/scenes/onboarding/sdks/sdksLogic.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdksLogic.tsx @@ -40,6 +40,8 @@ export const sdksLogic = kea([ setSourceOptions: (sourceOptions: LemonSelectOptions) => ({ sourceOptions }), resetSDKs: true, setAvailableSDKInstructionsMap: (sdkInstructionMap: SDKInstructionsMap) => ({ sdkInstructionMap }), + setShowSideBySide: (showSideBySide: boolean) => ({ showSideBySide }), + setPanel: (panel: 'instructions' | 'options') => ({ panel }), }), reducers({ sourceFilter: [ @@ -72,6 +74,18 @@ export const sdksLogic = kea([ setAvailableSDKInstructionsMap: (_, { sdkInstructionMap }) => sdkInstructionMap, }, ], + showSideBySide: [ + null as boolean | null, + { + setShowSideBySide: (_, { showSideBySide }) => showSideBySide, + }, + ], + panel: [ + 'options' as 'instructions' | 'options', + { + setPanel: (_, { panel }) => panel, + }, + ], }), selectors({ showSourceOptionsSelect: [ @@ -100,7 +114,7 @@ export const sdksLogic = kea([ actions.filterSDKs() }, setSDKs: () => { - if (!values.selectedSDK) { + if (!values.selectedSDK && values.showSideBySide == true) { actions.setSelectedSDK(values.sdks?.[0] || null) } }, @@ -118,6 +132,16 @@ export const sdksLogic = kea([ actions.setSourceFilter(null) actions.setSourceOptions(getSourceOptions(values.availableSDKInstructionsMap)) }, + setSelectedSDK: () => { + if (values.selectedSDK) { + actions.setPanel('instructions') + } + }, + setShowSideBySide: () => { + if (values.showSideBySide && !values.selectedSDK) { + actions.setSelectedSDK(values.sdks?.[0] || null) + } + }, })), events(({ actions }) => ({ afterMount: () => { diff --git a/frontend/src/scenes/products/Products.tsx b/frontend/src/scenes/products/Products.tsx index 11f9d4c0c1132..66994f4ab0f72 100644 --- a/frontend/src/scenes/products/Products.tsx +++ b/frontend/src/scenes/products/Products.tsx @@ -49,14 +49,26 @@ function OnboardingCompletedButton({ ) } -function OnboardingNotCompletedButton({ url, productKey }: { url: string; productKey: ProductKey }): JSX.Element { +function OnboardingNotCompletedButton({ + url, + productKey, + getStartedActionOverride, +}: { + url: string + productKey: ProductKey + getStartedActionOverride?: () => void +}): JSX.Element { const { onSelectProduct } = useActions(productsLogic) return ( { - onSelectProduct(productKey) - router.actions.push(url) + if (getStartedActionOverride) { + getStartedActionOverride() + } else { + onSelectProduct(productKey) + router.actions.push(url) + } }} > Get started @@ -68,19 +80,36 @@ export function getProductIcon(iconKey?: string | null, className?: string): JSX return Icons[iconKey || 'IconLogomark']({ className }) } -function ProductCard({ product }: { product: BillingProductV2Type }): JSX.Element { +export function ProductCard({ + product, + getStartedActionOverride, + orientation = 'vertical', + className, +}: { + product: BillingProductV2Type + getStartedActionOverride?: () => void + orientation?: 'horizontal' | 'vertical' + className?: string +}): JSX.Element { const { currentTeam } = useValues(teamLogic) const onboardingCompleted = currentTeam?.has_completed_onboarding_for?.[product.type] + const vertical = orientation === 'vertical' + return ( - -
-
{getProductIcon(product.icon_key, 'text-2xl')}
+ +
+
+
{getProductIcon(product.icon_key, 'text-2xl')}
+
-
-

{product.name}

+
+

{product.name}

+

{product.description}

-

{product.description}

-
+
{onboardingCompleted ? ( ) : ( - +
+ +
)}