mirror of https://github.com/langgenius/dify.git
feat: enhance banner tracking with impression and click events (#33926)
Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
parent
fdc880bc67
commit
2c8322c7b9
|
|
@ -1,3 +1,4 @@
|
|||
import type { ComponentProps } from 'react'
|
||||
import type { Banner } from '@/models/app'
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
|
@ -5,6 +6,11 @@ import { BannerItem } from '../banner-item'
|
|||
|
||||
const mockScrollTo = vi.fn()
|
||||
const mockSlideNodes = vi.fn()
|
||||
const mockTrackEvent = vi.fn()
|
||||
|
||||
vi.mock('@/app/components/base/amplitude', () => ({
|
||||
trackEvent: (...args: unknown[]) => mockTrackEvent(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/carousel', () => ({
|
||||
useCarousel: () => ({
|
||||
|
|
@ -48,19 +54,34 @@ class MockResizeObserver {
|
|||
}
|
||||
}
|
||||
|
||||
const renderBannerItem = (
|
||||
banner: Banner = createMockBanner(),
|
||||
props: Partial<ComponentProps<typeof BannerItem>> = {},
|
||||
) => {
|
||||
return render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
sort={1}
|
||||
language="en-US"
|
||||
{...props}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('BannerItem', () => {
|
||||
let mockWindowOpen: ReturnType<typeof vi.spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
mockWindowOpen = vi.spyOn(window, 'open').mockImplementation(() => null)
|
||||
mockSlideNodes.mockReturnValue([{}, {}, {}]) // 3 slides
|
||||
mockSlideNodes.mockReturnValue([{}, {}, {}])
|
||||
|
||||
vi.stubGlobal('ResizeObserver', MockResizeObserver)
|
||||
|
||||
Object.defineProperty(window, 'innerWidth', {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
value: 1400, // Above RESPONSIVE_BREAKPOINT (1200)
|
||||
value: 1400,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -73,81 +94,51 @@ describe('BannerItem', () => {
|
|||
|
||||
describe('basic rendering', () => {
|
||||
it('renders banner category', () => {
|
||||
const banner = createMockBanner()
|
||||
render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
|
||||
renderBannerItem()
|
||||
expect(screen.getByText('Featured')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders banner title', () => {
|
||||
const banner = createMockBanner()
|
||||
render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
|
||||
renderBannerItem()
|
||||
expect(screen.getByText('Test Banner Title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders banner description', () => {
|
||||
const banner = createMockBanner()
|
||||
render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
|
||||
renderBannerItem()
|
||||
expect(screen.getByText('Test banner description text')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders banner image with correct src and alt', () => {
|
||||
const banner = createMockBanner()
|
||||
render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
|
||||
renderBannerItem()
|
||||
const image = screen.getByRole('img')
|
||||
expect(image).toHaveAttribute('src', 'https://example.com/image.png')
|
||||
expect(image).toHaveAttribute('alt', 'Test Banner Title')
|
||||
})
|
||||
|
||||
it('renders view more text', () => {
|
||||
const banner = createMockBanner()
|
||||
render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
|
||||
renderBannerItem()
|
||||
expect(screen.getByText('explore.banner.viewMore')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('click handling', () => {
|
||||
it('opens banner link in new tab when clicked', () => {
|
||||
it('opens banner link in new tab and tracks click when clicked', () => {
|
||||
const banner = createMockBanner({ link: 'https://test-link.com' })
|
||||
render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
renderBannerItem(banner, { sort: 2, language: 'zh-Hans', accountId: 'account-123' })
|
||||
|
||||
const bannerElement = screen.getByText('Test Banner Title').closest('div[class*="cursor-pointer"]')
|
||||
fireEvent.click(bannerElement!)
|
||||
|
||||
expect(mockTrackEvent).toHaveBeenCalledWith('explore_banner_click', expect.objectContaining({
|
||||
banner_id: 'banner-1',
|
||||
title: 'Test Banner Title',
|
||||
sort: 2,
|
||||
link: 'https://test-link.com',
|
||||
page: 'explore',
|
||||
language: 'zh-Hans',
|
||||
account_id: 'account-123',
|
||||
event_time: expect.any(Number),
|
||||
}))
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||
'https://test-link.com',
|
||||
'_blank',
|
||||
|
|
@ -155,18 +146,16 @@ describe('BannerItem', () => {
|
|||
)
|
||||
})
|
||||
|
||||
it('does not open window when banner has no link', () => {
|
||||
it('tracks click even when banner has no link', () => {
|
||||
const banner = createMockBanner({ link: '' })
|
||||
render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
renderBannerItem(banner)
|
||||
|
||||
const bannerElement = screen.getByText('Test Banner Title').closest('div[class*="cursor-pointer"]')
|
||||
fireEvent.click(bannerElement!)
|
||||
|
||||
expect(mockTrackEvent).toHaveBeenCalledWith('explore_banner_click', expect.objectContaining({
|
||||
link: '',
|
||||
}))
|
||||
expect(mockWindowOpen).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
|
@ -174,28 +163,13 @@ describe('BannerItem', () => {
|
|||
describe('slide indicators', () => {
|
||||
it('renders correct number of indicator buttons', () => {
|
||||
mockSlideNodes.mockReturnValue([{}, {}, {}])
|
||||
const banner = createMockBanner()
|
||||
render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
|
||||
const buttons = screen.getAllByRole('button')
|
||||
expect(buttons).toHaveLength(3)
|
||||
renderBannerItem()
|
||||
expect(screen.getAllByRole('button')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('renders indicator buttons with correct numbers', () => {
|
||||
mockSlideNodes.mockReturnValue([{}, {}, {}])
|
||||
const banner = createMockBanner()
|
||||
render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
|
||||
renderBannerItem()
|
||||
expect(screen.getByText('01')).toBeInTheDocument()
|
||||
expect(screen.getByText('02')).toBeInTheDocument()
|
||||
expect(screen.getByText('03')).toBeInTheDocument()
|
||||
|
|
@ -203,13 +177,7 @@ describe('BannerItem', () => {
|
|||
|
||||
it('calls scrollTo when indicator is clicked', () => {
|
||||
mockSlideNodes.mockReturnValue([{}, {}, {}])
|
||||
const banner = createMockBanner()
|
||||
render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
renderBannerItem()
|
||||
|
||||
const secondIndicator = screen.getByText('02').closest('button')
|
||||
fireEvent.click(secondIndicator!)
|
||||
|
|
@ -219,81 +187,39 @@ describe('BannerItem', () => {
|
|||
|
||||
it('renders no indicators when no slides', () => {
|
||||
mockSlideNodes.mockReturnValue([])
|
||||
const banner = createMockBanner()
|
||||
render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
|
||||
renderBannerItem()
|
||||
expect(screen.queryByRole('button')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('isPaused prop', () => {
|
||||
it('defaults isPaused to false', () => {
|
||||
const banner = createMockBanner()
|
||||
render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
|
||||
renderBannerItem()
|
||||
expect(screen.getByText('Test Banner Title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('accepts isPaused prop', () => {
|
||||
const banner = createMockBanner()
|
||||
render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
isPaused={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
renderBannerItem(createMockBanner(), { isPaused: true })
|
||||
expect(screen.getByText('Test Banner Title')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('responsive behavior', () => {
|
||||
it('sets up ResizeObserver on mount', () => {
|
||||
const banner = createMockBanner()
|
||||
render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
|
||||
renderBannerItem()
|
||||
expect(mockResizeObserverObserve).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('adds resize event listener on mount', () => {
|
||||
const addEventListenerSpy = vi.spyOn(window, 'addEventListener')
|
||||
const banner = createMockBanner()
|
||||
render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
|
||||
renderBannerItem()
|
||||
expect(addEventListenerSpy).toHaveBeenCalledWith('resize', expect.any(Function))
|
||||
addEventListenerSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('removes resize event listener on unmount', () => {
|
||||
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener')
|
||||
const banner = createMockBanner()
|
||||
const { unmount } = render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
const { unmount } = renderBannerItem()
|
||||
|
||||
unmount()
|
||||
|
||||
|
|
@ -308,14 +234,7 @@ describe('BannerItem', () => {
|
|||
value: 1000,
|
||||
})
|
||||
|
||||
const banner = createMockBanner()
|
||||
render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
|
||||
renderBannerItem()
|
||||
expect(screen.getByText('Test Banner Title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
|
@ -326,14 +245,7 @@ describe('BannerItem', () => {
|
|||
value: 800,
|
||||
})
|
||||
|
||||
const banner = createMockBanner()
|
||||
render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
|
||||
renderBannerItem()
|
||||
expect(screen.getByText('explore.banner.viewMore')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
@ -348,13 +260,8 @@ describe('BannerItem', () => {
|
|||
'img-src': 'https://example.com/img.png',
|
||||
},
|
||||
} as Partial<Banner>)
|
||||
render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
|
||||
renderBannerItem(banner)
|
||||
expect(screen.getByText('Very Long Category Name')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
|
@ -367,13 +274,8 @@ describe('BannerItem', () => {
|
|||
'img-src': 'https://example.com/img.png',
|
||||
},
|
||||
} as Partial<Banner>)
|
||||
render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
|
||||
renderBannerItem(banner)
|
||||
const titleElement = screen.getByText('A Very Long Title That Should Be Truncated Eventually')
|
||||
expect(titleElement).toHaveClass('line-clamp-2')
|
||||
})
|
||||
|
|
@ -387,13 +289,8 @@ describe('BannerItem', () => {
|
|||
'img-src': 'https://example.com/img.png',
|
||||
},
|
||||
} as Partial<Banner>)
|
||||
render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
|
||||
renderBannerItem(banner)
|
||||
const descriptionElement = screen.getByText(/A very long description/)
|
||||
expect(descriptionElement).toHaveClass('line-clamp-4')
|
||||
})
|
||||
|
|
@ -402,56 +299,26 @@ describe('BannerItem', () => {
|
|||
describe('slide calculation', () => {
|
||||
it('calculates next index correctly for first slide', () => {
|
||||
mockSlideNodes.mockReturnValue([{}, {}, {}])
|
||||
const banner = createMockBanner()
|
||||
render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
|
||||
const buttons = screen.getAllByRole('button')
|
||||
expect(buttons).toHaveLength(3)
|
||||
renderBannerItem()
|
||||
expect(screen.getAllByRole('button')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('handles single slide case', () => {
|
||||
mockSlideNodes.mockReturnValue([{}])
|
||||
const banner = createMockBanner()
|
||||
render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
|
||||
const buttons = screen.getAllByRole('button')
|
||||
expect(buttons).toHaveLength(1)
|
||||
renderBannerItem()
|
||||
expect(screen.getAllByRole('button')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('wrapper styling', () => {
|
||||
it('has cursor-pointer class', () => {
|
||||
const banner = createMockBanner()
|
||||
const { container } = render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
|
||||
const { container } = renderBannerItem()
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('cursor-pointer')
|
||||
})
|
||||
|
||||
it('has rounded-2xl class', () => {
|
||||
const banner = createMockBanner()
|
||||
const { container } = render(
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={5000}
|
||||
/>,
|
||||
)
|
||||
|
||||
const { container } = renderBannerItem()
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('rounded-2xl')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|||
import Banner from '../banner'
|
||||
|
||||
const mockUseGetBanners = vi.fn()
|
||||
const mockUseSelector = vi.fn()
|
||||
const mockTrackEvent = vi.fn()
|
||||
|
||||
vi.mock('@/service/use-explore', () => ({
|
||||
useGetBanners: (...args: unknown[]) => mockUseGetBanners(...args),
|
||||
|
|
@ -15,6 +17,14 @@ vi.mock('@/context/i18n', () => ({
|
|||
useLocale: () => 'en-US',
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useSelector: (...args: unknown[]) => mockUseSelector(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/amplitude', () => ({
|
||||
trackEvent: (...args: unknown[]) => mockTrackEvent(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/carousel', () => ({
|
||||
Carousel: Object.assign(
|
||||
({ children, onMouseEnter, onMouseLeave, className }: {
|
||||
|
|
@ -54,9 +64,12 @@ vi.mock('@/app/components/base/carousel', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('../banner-item', () => ({
|
||||
BannerItem: ({ banner, autoplayDelay, isPaused }: {
|
||||
BannerItem: ({ banner, autoplayDelay, isPaused, sort, language, accountId }: {
|
||||
banner: BannerType
|
||||
autoplayDelay: number
|
||||
sort: number
|
||||
language: string
|
||||
accountId?: string
|
||||
isPaused?: boolean
|
||||
}) => (
|
||||
<div
|
||||
|
|
@ -64,6 +77,9 @@ vi.mock('../banner-item', () => ({
|
|||
data-banner-id={banner.id}
|
||||
data-autoplay-delay={autoplayDelay}
|
||||
data-is-paused={isPaused}
|
||||
data-sort={sort}
|
||||
data-language={language}
|
||||
data-account-id={accountId}
|
||||
>
|
||||
BannerItem:
|
||||
{' '}
|
||||
|
|
@ -87,6 +103,11 @@ const createMockBanner = (id: string, status: string = 'enabled', title: string
|
|||
describe('Banner', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
mockUseSelector.mockImplementation(selector => selector({
|
||||
userProfile: {
|
||||
id: 'account-123',
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -235,6 +256,59 @@ describe('Banner', () => {
|
|||
|
||||
expect(screen.getByTestId('carousel')).toHaveClass('rounded-2xl')
|
||||
})
|
||||
|
||||
it('tracks enabled banner impressions with expected payload', () => {
|
||||
mockUseGetBanners.mockReturnValue({
|
||||
data: [
|
||||
createMockBanner('1', 'enabled', 'Enabled Banner 1'),
|
||||
createMockBanner('2', 'disabled', 'Disabled Banner'),
|
||||
createMockBanner('3', 'enabled', 'Enabled Banner 2'),
|
||||
],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
})
|
||||
|
||||
render(<Banner />)
|
||||
|
||||
expect(mockTrackEvent).toHaveBeenCalledTimes(2)
|
||||
expect(mockTrackEvent).toHaveBeenNthCalledWith(1, 'explore_banner_impression', expect.objectContaining({
|
||||
banner_id: '1',
|
||||
title: 'Enabled Banner 1',
|
||||
sort: 1,
|
||||
link: 'https://example.com',
|
||||
page: 'explore',
|
||||
language: 'en-US',
|
||||
account_id: 'account-123',
|
||||
event_time: expect.any(Number),
|
||||
}))
|
||||
expect(mockTrackEvent).toHaveBeenNthCalledWith(2, 'explore_banner_impression', expect.objectContaining({
|
||||
banner_id: '3',
|
||||
title: 'Enabled Banner 2',
|
||||
sort: 2,
|
||||
link: 'https://example.com',
|
||||
page: 'explore',
|
||||
language: 'en-US',
|
||||
account_id: 'account-123',
|
||||
event_time: expect.any(Number),
|
||||
}))
|
||||
})
|
||||
|
||||
it('does not track impressions when account id is unavailable', () => {
|
||||
mockUseSelector.mockImplementation(selector => selector({
|
||||
userProfile: {
|
||||
id: '',
|
||||
},
|
||||
}))
|
||||
mockUseGetBanners.mockReturnValue({
|
||||
data: [createMockBanner('1', 'enabled', 'Enabled Banner 1')],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
})
|
||||
|
||||
render(<Banner />)
|
||||
|
||||
expect(mockTrackEvent).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('hover behavior', () => {
|
||||
|
|
@ -435,8 +509,25 @@ describe('Banner', () => {
|
|||
|
||||
const bannerItems = screen.getAllByTestId('banner-item')
|
||||
expect(bannerItems[0]).toHaveAttribute('data-banner-id', '1')
|
||||
expect(bannerItems[0]).toHaveAttribute('data-sort', '1')
|
||||
expect(bannerItems[1]).toHaveAttribute('data-banner-id', '2')
|
||||
expect(bannerItems[1]).toHaveAttribute('data-sort', '2')
|
||||
expect(bannerItems[2]).toHaveAttribute('data-banner-id', '3')
|
||||
expect(bannerItems[2]).toHaveAttribute('data-sort', '3')
|
||||
})
|
||||
|
||||
it('passes tracking context to banner item', () => {
|
||||
mockUseGetBanners.mockReturnValue({
|
||||
data: [createMockBanner('1', 'enabled', 'Banner 1')],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
})
|
||||
|
||||
render(<Banner />)
|
||||
|
||||
const bannerItem = screen.getByTestId('banner-item')
|
||||
expect(bannerItem).toHaveAttribute('data-language', 'en-US')
|
||||
expect(bannerItem).toHaveAttribute('data-account-id', 'account-123')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type { Banner } from '@/models/app'
|
|||
import { RiArrowRightLine } from '@remixicon/react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import { useCarousel } from '@/app/components/base/carousel'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { IndicatorButton } from './indicator-button'
|
||||
|
|
@ -11,6 +12,9 @@ import { IndicatorButton } from './indicator-button'
|
|||
type BannerItemProps = {
|
||||
banner: Banner
|
||||
autoplayDelay: number
|
||||
sort: number
|
||||
language: string
|
||||
accountId?: string
|
||||
isPaused?: boolean
|
||||
}
|
||||
|
||||
|
|
@ -20,7 +24,14 @@ const INDICATOR_WIDTH = 20
|
|||
const INDICATOR_GAP = 8
|
||||
const MIN_VIEW_MORE_WIDTH = 480
|
||||
|
||||
export const BannerItem: FC<BannerItemProps> = ({ banner, autoplayDelay, isPaused = false }) => {
|
||||
export const BannerItem: FC<BannerItemProps> = ({
|
||||
banner,
|
||||
autoplayDelay,
|
||||
sort,
|
||||
language,
|
||||
accountId,
|
||||
isPaused = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { api, selectedIndex } = useCarousel()
|
||||
const { category, title, description, 'img-src': imgSrc } = banner.content
|
||||
|
|
@ -91,9 +102,21 @@ export const BannerItem: FC<BannerItemProps> = ({ banner, autoplayDelay, isPause
|
|||
|
||||
const handleBannerClick = useCallback(() => {
|
||||
incrementResetKey()
|
||||
|
||||
trackEvent('explore_banner_click', {
|
||||
banner_id: banner.id,
|
||||
title: banner.content.title,
|
||||
sort,
|
||||
link: banner.link,
|
||||
page: 'explore',
|
||||
language,
|
||||
account_id: accountId,
|
||||
event_time: Date.now(),
|
||||
})
|
||||
|
||||
if (banner.link)
|
||||
window.open(banner.link, '_blank', 'noopener,noreferrer')
|
||||
}, [banner.link, incrementResetKey])
|
||||
}, [accountId, banner, incrementResetKey, language, sort])
|
||||
|
||||
const handleIndicatorClick = useCallback((index: number) => {
|
||||
incrementResetKey()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import type { FC } from 'react'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import { Carousel } from '@/app/components/base/carousel'
|
||||
import { useSelector } from '@/context/app-context'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useGetBanners } from '@/service/use-explore'
|
||||
import Loading from '../../base/loading'
|
||||
|
|
@ -23,9 +25,11 @@ const LoadingState: FC = () => (
|
|||
const Banner: FC = () => {
|
||||
const locale = useLocale()
|
||||
const { data: banners, isLoading, isError } = useGetBanners(locale)
|
||||
const accountId = useSelector(s => s.userProfile.id)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [isResizing, setIsResizing] = useState(false)
|
||||
const resizeTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const trackedBannerIdsRef = useRef<Set<string>>(new Set())
|
||||
|
||||
const enabledBanners = useMemo(
|
||||
() => banners?.filter(banner => banner.status === 'enabled') ?? [],
|
||||
|
|
@ -56,6 +60,28 @@ const Banner: FC = () => {
|
|||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!accountId)
|
||||
return
|
||||
|
||||
enabledBanners.forEach((banner, index) => {
|
||||
if (trackedBannerIdsRef.current.has(banner.id))
|
||||
return
|
||||
|
||||
trackEvent('explore_banner_impression', {
|
||||
banner_id: banner.id,
|
||||
title: banner.content.title,
|
||||
sort: index + 1,
|
||||
link: banner.link,
|
||||
page: 'explore',
|
||||
language: locale,
|
||||
account_id: accountId,
|
||||
event_time: Date.now(),
|
||||
})
|
||||
trackedBannerIdsRef.current.add(banner.id)
|
||||
})
|
||||
}, [accountId, enabledBanners, locale])
|
||||
|
||||
if (isLoading)
|
||||
return <LoadingState />
|
||||
|
||||
|
|
@ -77,12 +103,15 @@ const Banner: FC = () => {
|
|||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<Carousel.Content>
|
||||
{enabledBanners.map(banner => (
|
||||
{enabledBanners.map((banner, index) => (
|
||||
<Carousel.Item key={banner.id}>
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={AUTOPLAY_DELAY}
|
||||
isPaused={isPaused}
|
||||
sort={index + 1}
|
||||
language={locale}
|
||||
accountId={accountId}
|
||||
/>
|
||||
</Carousel.Item>
|
||||
))}
|
||||
|
|
|
|||
Loading…
Reference in New Issue