diff --git a/assets/js/dashboard/components/feature-preview-mocks.tsx b/assets/js/dashboard/components/feature-preview-mocks.tsx new file mode 100644 index 000000000000..33460196284f --- /dev/null +++ b/assets/js/dashboard/components/feature-preview-mocks.tsx @@ -0,0 +1,101 @@ +import React from 'react' +import classNames from 'classnames' + +/* + * Non-interactive previews of the Funnels / Properties / Exploration reports, + * rendered behind the CTA when the feature isn't available on the current + * plan. They give users an at-a-glance idea of what the report looks like + * without needing to navigate elsewhere. Always heavily blurred in their + * parent container, so the goal is to be suggestive rather than pixel-perfect. + */ + +export function PropertiesPreviewMock() { + const rows = [98, 82, 68, 60, 45, 21, 12, 8, 5, 2] + + return ( +
+ {rows.map((width, i) => ( +
+ ))} +
+ ) +} + +export function FunnelsPreviewMock() { + const steps = [{ visitors: 100 }, { visitors: 55 }, { visitors: 24 }] + + return ( +
+
+ +
+ {steps.map((step, i) => ( +
+
+
+
+ ))} +
+ +
+ {steps.map((_, i) => ( +
+
+
+ ))} +
+
+ ) +} + +export function ExplorationPreviewMock() { + const columns = [ + [98, 96, 54, 42, 27, 18, 15, 10, 5], + [88, 70, 68, 30, 18, 16, 10, 9, 5], + [82, 78, 48, 35, 25, 14, 12, 10, 5] + ] + + return ( +
+ {columns.map((rows, colIdx) => ( +
+
+
+
+
+ {rows.map((width, i) => ( +
+ ))} +
+
+ ))} +
+ ) +} diff --git a/assets/js/dashboard/components/feature-setup-notice.tsx b/assets/js/dashboard/components/feature-setup-notice.tsx index 570b04a808c7..7fd9a349a8b4 100644 --- a/assets/js/dashboard/components/feature-setup-notice.tsx +++ b/assets/js/dashboard/components/feature-setup-notice.tsx @@ -1,20 +1,37 @@ import React from 'react' +import classNames from 'classnames' import { MODES } from '../stats/behaviours/modes-context' import * as api from '../api' import { useSiteContext } from '../site-context' +import { Pill } from './pill' +import { DiamondIcon } from './icons' +import { buttonClassName } from './button' + +function BusinessPill() { + return ( + + + Business + + ) +} export function FeatureSetupNotice({ feature, title, info, callToAction, - onHideAction + secondaryCallToAction, + onHideAction, + previewMock }: { feature: keyof typeof MODES title: React.ReactNode info: React.ReactNode callToAction: { link: string; action: string } - onHideAction: () => void + secondaryCallToAction?: { link: string; action: string } + onHideAction: (() => void) | null + previewMock?: React.ReactNode }) { const site = useSiteContext() const sectionTitle = MODES[feature].title @@ -30,7 +47,7 @@ export function FeatureSetupNotice({ method: 'PUT', body: { feature: feature } }) - .then(() => onHideAction()) + .then(() => onHideAction?.()) .catch((error) => { if (!(error instanceof api.ApiError)) { throw error @@ -43,23 +60,12 @@ export function FeatureSetupNotice({ return ( -

{callToAction.action}

- - - + {callToAction.action} →
) } @@ -68,24 +74,65 @@ export function FeatureSetupNotice({ return ( ) } + function renderSecondaryCallToAction() { + if (!secondaryCallToAction) { + return null + } + return ( + + {secondaryCallToAction.action} + + ) + } + return ( -
-
+
+ {previewMock && ( + + )} +
+ {previewMock && ( +
+ +
+ )}
{title}
-
+
{info}
-
- {renderHideButton()} +
+ {typeof onHideAction === 'function' && renderHideButton()} + {renderSecondaryCallToAction()} {renderCallToAction()}
diff --git a/assets/js/dashboard/components/icons.tsx b/assets/js/dashboard/components/icons.tsx index 281f1f6a58ea..0439666850bc 100644 --- a/assets/js/dashboard/components/icons.tsx +++ b/assets/js/dashboard/components/icons.tsx @@ -133,6 +133,23 @@ export const FolderIcon = ({ className }: { className?: string }) => ( ) +export const DiamondIcon = ({ className }: { className?: string }) => ( + + + +) + export const Spinner = ({ className }: { className?: string }) => ( = { + green: 'bg-green-50 dark:bg-green-900/60 text-green-700 dark:text-green-300', + yellow: + 'bg-yellow-100 dark:bg-yellow-900/40 text-yellow-600 dark:text-yellow-400' +} + export type PillProps = { className?: string + color?: PillColor children: ReactNode } -export function Pill({ className, children }: PillProps) { +export function Pill({ className, color = 'green', children }: PillProps) { return (
diff --git a/assets/js/dashboard/stats/behaviours/index.js b/assets/js/dashboard/stats/behaviours/index.js index 8ffca74f1624..c611b3bb35b7 100644 --- a/assets/js/dashboard/stats/behaviours/index.js +++ b/assets/js/dashboard/stats/behaviours/index.js @@ -3,6 +3,11 @@ import * as storage from '../../util/storage' import ImportedQueryUnsupportedWarning from '../imported-query-unsupported-warning' import Properties from './props' import { FeatureSetupNotice } from '../../components/feature-setup-notice' +import { + ExplorationPreviewMock, + FunnelsPreviewMock, + PropertiesPreviewMock +} from '../../components/feature-preview-mocks' import { hasConversionGoalFilter, getGoalFilter, @@ -32,7 +37,7 @@ import { getSpecialGoal, isPageViewGoal, isSpecialGoal } from '../../util/goals' /*global BUILD_EXTRA*/ /*global require*/ -function maybeRequire() { +function maybeRequireFunnels() { if (BUILD_EXTRA) { // eslint-disable-next-line @typescript-eslint/no-require-imports return require('../../extra/funnel') @@ -46,12 +51,12 @@ function maybeRequireExploration() { // eslint-disable-next-line @typescript-eslint/no-require-imports return require('../../extra/exploration') } else { - return { FunnelExploration: null } + return { default: null } } } -const Funnel = maybeRequire().default -const { FunnelExploration } = maybeRequireExploration() +const Funnel = maybeRequireFunnels().default +const FunnelExploration = maybeRequireExploration().default function singleGoalFilterApplied(dashboardState) { const goalFilter = getGoalFilter(dashboardState) @@ -304,7 +309,29 @@ function Behaviours({ importedDataInView, setMode, mode }) { if (FunnelExploration === null) { return featureUnavailable() } - return + + if (site.explorationAvailable) { + return + } + + const callToAction = { action: 'Upgrade', link: '/billing/choose-plan' } + + return ( + } + /> + ) } function renderFunnels() { @@ -327,12 +354,15 @@ function Behaviours({ importedDataInView, setMode, mode }) { return ( disableMode(Mode.FUNNELS)} + previewMock={ + !site.funnelsAvailable ? : undefined + } /> ) } else { @@ -360,12 +390,15 @@ function Behaviours({ importedDataInView, setMode, mode }) { return ( disableMode(Mode.PROPS)} + previewMock={ + !site.propsAvailable ? : undefined + } /> ) } else { @@ -383,8 +416,14 @@ function Behaviours({ importedDataInView, setMode, mode }) { function featureUnavailable() { return ( -
- This feature is unavailable +
+ This report is available in Plausible Cloud + + Learn more +
) } @@ -537,7 +576,7 @@ function Behaviours({ importedDataInView, setMode, mode }) { Funnels ))} - {!site.isConsolidatedView && site.explorationAvailable && ( + {!site.isConsolidatedView && isEnabled(Mode.EXPLORATION) && (