Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,34 @@ import {
TutorialCardWithAuthElements,
} from 'components/tutorial-card'
import s from './tutorial-cards.module.css'
import compactStyles from 'components/tutorial-card/tutorial-card-compact.module.css'

interface TutorialCardsGridListProps extends CardsGridListProps {
tutorials: TutorialCardPropsWithId[]
compact?: boolean
}

/**
* Handles rendering a grid of Tutorial cards, and pre-fetching the
* `isBookmarked` state for each card.
*/
const TutorialCardsGridList = ({ tutorials, ...restProps }) => {
/**
* Collect the `tutorialIds` and React elements to render in separate arrays
* at the same time (to save on iterating over the same data twice).
*/
const TutorialCardsGridList = ({
tutorials,
compact = false,
...restProps
}) => {
const tutorialIds = []
const cardsGridListItems = []
tutorials.forEach((tutorial: TutorialCardPropsWithId) => {
tutorialIds.push(tutorial.id)
cardsGridListItems.push(
<div className={s.sandboxCardBox} key={tutorial.id}>
<TutorialCardWithAuthElements {...tutorial} />
<div
className={`${s.tutorialCardBox} ${
compact ? compactStyles.compactTutorialCard : ''
}`}
key={tutorial.id}
>
<TutorialCardWithAuthElements {...tutorial} hasBookmark={false} />
</div>
)
})

/**
* Prime the `isBookmarked` queries for the tutorial cards we know we need to
* render via collected `tutorialIds` array.
*/
const { isFetching, isRefetching } = useBookmarksByTutorialIds({
tutorialIds,
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
.sandboxCardBox {
min-height: 200px;
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

.tutorialCardBox {
min-height: 100px;
height: 100%;
display: flex;
flex-direction: column;
Expand Down
2 changes: 1 addition & 1 deletion src/components/cards-grid-list/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface CardsGridListProps {
children: ReactNode
isOrdered?: boolean
fixedColumns?: number
gridGap?: '16px' | '24px'
gridGap?: '8px' | '12px' | '16px' | '24px'
}

export type { CardsGridListProps }
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { IconAlertDiamond24 } from '@hashicorp/flight-icons/svg-react/alert-diam
import { MdxHighlight, MdxTip, MdxNote, MdxWarning } from './variants'
import { MdxInlineAlertData, MdxInlineAlertProps } from './types'
import s from './mdx-inline-alert.module.css'
import posthog from 'posthog-js'

const ALERT_DATA: MdxInlineAlertData = {
tip: { title: 'Tip', icon: <IconInfo24 />, color: 'neutral' },
Expand Down Expand Up @@ -73,8 +74,8 @@ export const MdxInlineAlert = withErrorBoundary(
MdxInlineAlertBase,
AlertErrorFallback,
(error, errorInfo) => {
if (typeof window !== 'undefined' && window.posthog?.capture) {
window.posthog.capture('mdx_component_error', {
if (typeof window !== 'undefined' && posthog?.capture) {
posthog.capture('mdx_component_error', {
component_name: 'MdxInlineAlert',
error_message: error.message,
error_stack: error.stack,
Expand Down
4 changes: 4 additions & 0 deletions src/components/interactive-lab-callout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useInstruqtEmbed } from 'contexts/instruqt-lab'
import { FC } from 'react'
import s from './interactive-lab-callout.module.css'
import SANDBOX_CONFIG from 'content/sandbox/sandbox.json' assert { type: 'json' }
import { trackSandboxInteraction } from 'views/sandbox-view'

interface InteractiveLabCalloutProps {
labId?: string
Expand All @@ -34,6 +35,9 @@ const InteractiveLabCallout: FC<InteractiveLabCalloutProps> = ({ labId }) => {

const handleStartLab = () => {
if (effectiveLabId) {
trackSandboxInteraction('click', effectiveLabId, {
source: 'interactive-lab-callout',
})
ctx.openLab(effectiveLabId)
ctx.setActive(true)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ vi.mock('next/router', () => ({
vi.mock('lib/posthog-events', () => ({
trackSandboxEvent: vi.fn(),
SANDBOX_EVENT: {
SANDBOX_STARTED: 'sandbox_started',
SANDBOX_OPEN: 'sandbox_open',
SANDBOX_CLOSED: 'sandbox_closed',
SANDBOX_LOADED: 'sandbox_loaded',
SANDBOX_ERROR: 'sandbox_error',
SANDBOX_RETRY: 'sandbox_retry',
Expand Down Expand Up @@ -133,7 +136,7 @@ describe('EmbedElement', () => {
screen.queryByText('Loading your sandbox...')
).not.toBeInTheDocument()

expect(mockTrackSandboxEvent).toHaveBeenCalledWith('sandbox_loaded', {
expect(mockTrackSandboxEvent).toHaveBeenCalledWith('sandbox_open', {
labId: 'test-lab-id',
page: '/test-path',
})
Expand Down
2 changes: 1 addition & 1 deletion src/components/lab-embed/embed-element/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const EmbedElement = memo(function EmbedElement(): JSX.Element {
}))

if (labId) {
trackSandboxEvent(SANDBOX_EVENT.SANDBOX_LOADED, {
trackSandboxEvent(SANDBOX_EVENT.SANDBOX_OPEN, {
labId,
page: router.asPath,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { render, screen, fireEvent } from '@testing-library/react'
import SandboxDropdown from '../index'

// Mock the hooks
// Mock the hooks and functions
const mockUserRouter = vi.fn()
vi.mock('next/router', () => ({
useRouter: () => mockUserRouter(),
Expand All @@ -22,6 +22,20 @@ vi.mock('contexts/instruqt-lab', () => ({
useInstruqtEmbed: () => mockUseInstruqtEmbed(),
}))

const mockTrackSandboxInteraction = vi.fn()
vi.mock('views/sandbox-view', () => ({
trackSandboxInteraction: (...args: unknown[]) =>
mockTrackSandboxInteraction(...args),
}))

const mockTrackSandboxEvent = vi.fn()
vi.mock('lib/posthog-events', () => ({
trackSandboxEvent: (...args: unknown[]) => mockTrackSandboxEvent(...args),
SANDBOX_EVENT: {
SANDBOX_OPEN: 'sandbox_open',
},
}))

describe('SandboxDropdown', () => {
beforeEach(() => {
// Reset all mocks before each test
Expand Down Expand Up @@ -135,7 +149,7 @@ describe('SandboxDropdown', () => {
expect(mockOpenLab).toHaveBeenCalled()
})

it('tracks sandbox events when opening labs', () => {
it('tracks sandbox events and interactions when clicking a lab', () => {
const mockOpenLab = vi.fn()
const mockSetActive = vi.fn()
mockUseInstruqtEmbed.mockImplementation(() => ({
Expand All @@ -155,5 +169,20 @@ describe('SandboxDropdown', () => {

// Verify openLab was called
expect(mockOpenLab).toHaveBeenCalled()

// Verify tracking events were called
expect(mockTrackSandboxEvent).toHaveBeenCalledWith('sandbox_open', {
labId: expect.any(String),
page: '/',
})

// Verify interaction tracking
expect(mockTrackSandboxInteraction).toHaveBeenCalledWith(
'hover',
expect.any(String),
{
page: '/',
}
)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { SandboxLab } from 'types/sandbox'
import { ProductSlug } from 'types/products'
import { buildLabIdWithConfig } from 'lib/build-instruqt-url'
import { useTheme } from 'next-themes'
import { trackSandboxInteraction } from 'views/sandbox-view'

interface SandboxDropdownProps {
ariaLabel: string
Expand Down Expand Up @@ -122,9 +123,6 @@ const SandboxDropdown = ({ ariaLabel, label }: SandboxDropdownProps) => {
}
}

/**
* Handle lab selection
*/
const handleLabClick = (lab: SandboxLab) => {
const labWithTrack = {
...lab,
Expand All @@ -133,16 +131,13 @@ const SandboxDropdown = ({ ariaLabel, label }: SandboxDropdownProps) => {
const fullLabId = buildLabIdWithConfig(labWithTrack)
openLab(fullLabId)
setActive(true)
trackSandboxEvent(SANDBOX_EVENT.SANDBOX_STARTED, {
trackSandboxEvent(SANDBOX_EVENT.SANDBOX_OPEN, {
labId: fullLabId,
page: router.asPath,
})
setIsOpen(false)
}

/**
* Navigate to the sandbox page
*/
const navigateToSandboxPage = (e: React.MouseEvent) => {
e.preventDefault()
router.push(`/${currentProduct.slug}/sandbox`)
Expand Down Expand Up @@ -206,12 +201,12 @@ const SandboxDropdown = ({ ariaLabel, label }: SandboxDropdownProps) => {
<div className={s.introSandboxRow}>
<ProductIcon
productSlug={currentProduct.slug as ProductSlug}
size={24}
size={16}
className={s.productIcon}
/>
<Text
asElement="span"
className={`${s.sectionTitle} ${s.introSandboxTitle}`}
className={`${s.sectionTitle} ${s.introSandboxTitle} ${s.title}`}
size={200}
weight="semibold"
>
Expand All @@ -220,7 +215,7 @@ const SandboxDropdown = ({ ariaLabel, label }: SandboxDropdownProps) => {
</div>
<Text
asElement="span"
className={`${s.introText} ${s.introSandboxText}`}
className={`${s.introText} ${s.introSandboxText} ${s.description}`}
size={100}
weight="regular"
>
Expand All @@ -230,7 +225,6 @@ const SandboxDropdown = ({ ariaLabel, label }: SandboxDropdownProps) => {
</Text>
</button>

{/* Available Product Sandboxes Section */}
<Text
asElement="p"
className={s.sectionTitle}
Expand All @@ -245,7 +239,12 @@ const SandboxDropdown = ({ ariaLabel, label }: SandboxDropdownProps) => {
<li key={lab.labId || index} className={s.itemContainer}>
<button
className={s.sandboxItem}
onClick={() => handleLabClick(lab)}
onClick={() => {
handleLabClick(lab)
trackSandboxInteraction('hover', lab.labId, {
page: router.asPath,
})
}}
onKeyDown={handleKeyDown}
>
<div className={s.content}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,16 @@
.productIcon {
flex-shrink: 0;
color: var(--token-color-foreground-faint);
width: 16px;
height: 16px;
}

.description {
color: var(--token-color-foreground-faint);
display: block;
line-height: 1.5;
font-size: 14px;
font-size: 13px;
font-weight: 400;
}

.learnMoreLink {
Expand Down
72 changes: 72 additions & 0 deletions src/components/sandbox-card/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

import React from 'react'
import { ProductSlug } from 'types/products'
import Card from 'components/card'
import CardTitle from 'components/card/components/card-title'
import CardDescription from 'components/card/components/card-description'
import CardFooter from 'components/card/components/card-footer'
import ButtonLink from 'components/button-link'
import ProductIcon from 'components/product-icon'
import s from './sandbox-card.module.css'
import { trackSandboxInteraction } from 'views/sandbox-view'

export interface SandboxCardProps {
title: string
description: string
labId: string
products: string[]
onLaunch: () => void
className?: string
clickBehavior?: 'card' | 'button'
}

const SandboxCard: React.FC<SandboxCardProps> = ({
title,
description,
labId,
products,
onLaunch,
className,
}) => {
return (
<div className={`${s.sandboxCardWrapper} ${className || ''}`}>
<Card className={s.sandboxCard}>
<div className={s.cardHeader}>
<CardTitle text={title} />
<div className={s.productIcons}>
{products.map((productSlug) => (
<ProductIcon
key={`product-${labId}-${productSlug}`}
productSlug={productSlug as ProductSlug}
size={16}
className={s.productIcon}
/>
))}
</div>
</div>
<CardDescription text={description} className={s.description} />
<CardFooter className={s.footer}>
<ButtonLink
href="#"
className={s.launchButton}
aria-label={`Launch ${title} Sandbox`}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
trackSandboxInteraction('click', labId, { lab_title: title })
onLaunch()
}}
size="medium"
text="Launch Sandbox"
/>
</CardFooter>
</Card>
</div>
)
}

export default SandboxCard
Loading
Loading