Skip to content
101 changes: 101 additions & 0 deletions assets/js/dashboard/components/feature-preview-mocks.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="size-full flex flex-col gap-1.5 pt-9">
{rows.map((width, i) => (
<div
key={i}
className="h-7.5 bg-red-200/70 dark:bg-gray-500/30 rounded-sm"
style={{ width: `${width}%` }}
/>
))}
</div>
)
}

export function FunnelsPreviewMock() {
const steps = [{ visitors: 100 }, { visitors: 55 }, { visitors: 24 }]

return (
<div className="size-full flex flex-col pt-5">
<div className="h-4 w-32 bg-gray-400/60 dark:bg-gray-600 rounded-md mb-8" />

<div className="flex-1 flex items-end justify-between lg:justify-around gap-6">
{steps.map((step, i) => (
<div
key={i}
className="relative h-full flex-1 max-w-[160px] flex flex-col justify-end rounded-md overflow-hidden"
>
<div
className="w-full bg-indigo-100 dark:bg-gray-700"
style={{ height: `${100 - step.visitors}%` }}
/>
<div
className="w-full bg-indigo-500"
style={{ height: `${step.visitors}%` }}
/>
</div>
))}
</div>

<div className="flex justify-around gap-6 pt-4 pb-2">
{steps.map((_, i) => (
<div key={i} className="flex-1 max-w-[160px] flex justify-center">
<div className="h-3 w-3/5 bg-gray-400/70 dark:bg-gray-600 rounded-md" />
</div>
))}
</div>
</div>
)
}

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 (
<div className="size-full flex gap-3 pt-3">
{columns.map((rows, colIdx) => (
<div
key={colIdx}
className={classNames(
'flex-1 flex-col rounded-lg border border-gray-400 dark:border-gray-500 overflow-hidden',
colIdx === 2 ? 'hidden lg:flex' : 'flex'
)}
>
<div className="h-12 shrink-0 flex items-center px-3">
<div className="h-2.5 w-20 bg-gray-400/70 dark:bg-gray-500/80 rounded" />
</div>
<div className="flex flex-col gap-1.5 px-2 pb-2">
{rows.map((width, i) => (
<div
key={i}
className={
i === 0 && colIdx === 0
? 'h-7.5 bg-indigo-300/80 dark:bg-indigo-500/70 rounded-sm'
: 'h-7.5 bg-indigo-200/70 dark:bg-indigo-500/30 rounded-sm'
}
style={{ width: `${width}%` }}
/>
))}
</div>
</div>
))}
</div>
)
}
97 changes: 72 additions & 25 deletions assets/js/dashboard/components/feature-setup-notice.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Pill color="yellow">
<DiamondIcon className="size-3.5 [&_path]:stroke-2" />
Business
</Pill>
)
}

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
Expand All @@ -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
Expand All @@ -43,23 +60,12 @@ export function FeatureSetupNotice({
return (
<a
href={callToAction.link}
className="flex items-center gap-x-1.5 ml-2 sm:ml-4 button px-2 sm:px-4"
className={buttonClassName({
theme: 'primary',
className: 'ml-2 sm:ml-4'
})}
>
<p className="text-xs sm:text-sm font-medium">{callToAction.action}</p>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75"
/>
</svg>
{callToAction.action} &rarr;
</a>
)
}
Expand All @@ -68,24 +74,65 @@ export function FeatureSetupNotice({
return (
<button
onClick={requestHideSection}
className="inline-block px-2 sm:px-4 py-2 font-medium leading-5 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-100 hover:text-gray-900 dark:hover:bg-gray-600 dark:hover:text-white hover:shadow-sm transition-all duration-150"
className={buttonClassName({ theme: 'secondary' })}
>
Hide this report
</button>
)
}

function renderSecondaryCallToAction() {
if (!secondaryCallToAction) {
return null
}
return (
<a
href={secondaryCallToAction.link}
className={buttonClassName({
theme: 'secondary'
})}
>
{secondaryCallToAction.action}
</a>
)
}

return (
<div className="size-full flex items-center justify-center">
<div className="py-3 max-w-2xl">
<div
className={classNames(
'relative size-full flex items-center justify-center',
previewMock && 'md:h-[400px]'
)}
>
{previewMock && (
<div
aria-hidden="true"
className="hidden md:block pointer-events-none absolute inset-0 blur-sm opacity-50"
>
{previewMock}
</div>
)}
<div
className={classNames(
'relative py-3 max-w-2xl',
previewMock &&
'max-w-lg md:p-8 md:bg-white md:dark:bg-gray-800 md:border md:border-gray-100 md:dark:border-gray-750 md:rounded-lg md:shadow-xl'
)}
>
{previewMock && (
<div className="flex justify-center mb-3">
<BusinessPill />
</div>
)}
<div className="text-center mt-2 text-gray-800 dark:text-gray-200 font-medium text-pretty">
{title}
</div>
<div className="text-center mt-4 font-small text-sm text-gray-500 dark:text-gray-200 text-pretty">
<div className="text-center mt-4 font-small text-sm text-gray-500 dark:text-gray-300 text-pretty">
{info}
</div>
<div className="text-xs sm:text-sm flex my-6 justify-center">
{renderHideButton()}
<div className="text-xs sm:text-sm flex mt-6 mb-1 justify-center">
{typeof onHideAction === 'function' && renderHideButton()}
{renderSecondaryCallToAction()}
{renderCallToAction()}
</div>
</div>
Expand Down
17 changes: 17 additions & 0 deletions assets/js/dashboard/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,23 @@ export const FolderIcon = ({ className }: { className?: string }) => (
</svg>
)

export const DiamondIcon = ({ className }: { className?: string }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className={className}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M2.734 9h18.531M3.023 8.164l3.205-3.408c.255-.27.61-.424.984-.424h9.57c.374 0 .73.153.985.424l3.205 3.408c.44.468.48 1.18.093 1.693l-7.99 10.608a1.352 1.352 0 0 1-2.155 0L2.93 9.857a1.31 1.31 0 0 1 .093-1.693"
/>
</svg>
)

export const Spinner = ({ className }: { className?: string }) => (
<svg
className={className}
Expand Down
14 changes: 12 additions & 2 deletions assets/js/dashboard/components/pill.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import React, { ReactNode } from 'react'
import classNames from 'classnames'

export type PillColor = 'green' | 'yellow'

const colorClasses: Record<PillColor, 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 (
<div
className={classNames(
'flex items-center shrink-0 h-fit rounded-md bg-green-50 dark:bg-green-900/60 text-green-700 dark:text-green-300 text-xs font-medium px-2.5 py-1',
'flex items-center gap-x-1 shrink-0 h-fit rounded-md text-xs font-medium px-2.5 py-1',
colorClasses[color],
className
)}
>
Expand Down
Loading
Loading