Add view traffic charts to business home and super-admin.
Wire product, website, portfolio, and blog 30-day view charts to the analytics API and expose the new chart types in super-admin business settings. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
468bb87fcf
commit
202ac7d4bd
@@ -25,6 +25,8 @@ interface DailyActivityChartProps {
|
||||
load: (signal: AbortSignal) => Promise<DualDailyActivityResponse>
|
||||
/** Day bars (default) or roll up into last 12 months. */
|
||||
granularity?: 'day' | 'month'
|
||||
/** Hide the secondary series (for single-metric view charts). */
|
||||
showSecondary?: boolean
|
||||
}
|
||||
|
||||
function formatDayLabel(dateKey: string, locale: string): string {
|
||||
@@ -47,6 +49,7 @@ export function DailyActivityChart({
|
||||
secondaryBarTitleKey,
|
||||
load,
|
||||
granularity = 'day',
|
||||
showSecondary = true,
|
||||
}: DailyActivityChartProps) {
|
||||
const t = useT()
|
||||
const { locale } = useLocale()
|
||||
@@ -120,10 +123,12 @@ export function DailyActivityChart({
|
||||
<span className={`${styles.legendDot} ${styles.tonePrimary}`} />
|
||||
{t(primaryLegendKey, { count: primaryTotal })}
|
||||
</span>
|
||||
<span className={styles.legendItem}>
|
||||
<span className={`${styles.legendDot} ${styles.toneAccent}`} />
|
||||
{t(secondaryLegendKey, { count: secondaryTotal })}
|
||||
</span>
|
||||
{showSecondary ? (
|
||||
<span className={styles.legendItem}>
|
||||
<span className={`${styles.legendDot} ${styles.toneAccent}`} />
|
||||
{t(secondaryLegendKey, { count: secondaryTotal })}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -159,17 +164,19 @@ export function DailyActivityChart({
|
||||
count: item.primary,
|
||||
})}
|
||||
/>
|
||||
<div
|
||||
className={`${styles.bar} ${styles.barAccent} ${styles.barMonth}`}
|
||||
style={{
|
||||
height: Math.max(secondaryHeight, item.secondary > 0 ? 4 : 0),
|
||||
}}
|
||||
title={t(secondaryBarTitleKey, {
|
||||
day: item.label,
|
||||
month: item.label,
|
||||
count: item.secondary,
|
||||
})}
|
||||
/>
|
||||
{showSecondary ? (
|
||||
<div
|
||||
className={`${styles.bar} ${styles.barAccent} ${styles.barMonth}`}
|
||||
style={{
|
||||
height: Math.max(secondaryHeight, item.secondary > 0 ? 4 : 0),
|
||||
}}
|
||||
title={t(secondaryBarTitleKey, {
|
||||
day: item.label,
|
||||
month: item.label,
|
||||
count: item.secondary,
|
||||
})}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<span
|
||||
className={styles.label}
|
||||
@@ -196,16 +203,18 @@ export function DailyActivityChart({
|
||||
style={{ height: Math.max(primaryHeight, item.count > 0 ? 4 : 0) }}
|
||||
title={t(primaryBarTitleKey, { day: label, count: item.count })}
|
||||
/>
|
||||
<div
|
||||
className={`${styles.bar} ${styles.barAccent}`}
|
||||
style={{
|
||||
height: Math.max(secondaryHeight, secondaryCount > 0 ? 4 : 0),
|
||||
}}
|
||||
title={t(secondaryBarTitleKey, {
|
||||
day: label,
|
||||
count: secondaryCount,
|
||||
})}
|
||||
/>
|
||||
{showSecondary ? (
|
||||
<div
|
||||
className={`${styles.bar} ${styles.barAccent}`}
|
||||
style={{
|
||||
height: Math.max(secondaryHeight, secondaryCount > 0 ? 4 : 0),
|
||||
}}
|
||||
title={t(secondaryBarTitleKey, {
|
||||
day: label,
|
||||
count: secondaryCount,
|
||||
})}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<span className={styles.label} lang="en" dir="ltr">
|
||||
{label}
|
||||
|
||||
@@ -4,36 +4,93 @@ import { ProductActivityChart } from './ProductActivityChart'
|
||||
import {
|
||||
getCustomersDailyActivity,
|
||||
getOrdersDailyActivity,
|
||||
getViewDailyActivity,
|
||||
type WebsiteViewKind,
|
||||
} from '../services/dailyActivityService'
|
||||
import type { HomeChartId } from '../utils/businessModules'
|
||||
import { useT } from '../i18n/useT'
|
||||
import styles from './DailyActivityChart.module.css'
|
||||
import type { BusinessMessageKey } from '../i18n/messages'
|
||||
|
||||
interface HomeChartSlotProps {
|
||||
chartId: HomeChartId
|
||||
}
|
||||
|
||||
function HomeChartPlaceholder({ chartId }: { chartId: HomeChartId }) {
|
||||
const t = useT()
|
||||
const titleKey =
|
||||
chartId === 'blog_views_30d'
|
||||
? 'home.chart.blogViews.title'
|
||||
: 'home.chart.placeholder.title'
|
||||
const subtitleKey =
|
||||
chartId === 'blog_views_30d'
|
||||
? 'home.chart.blogViews.subtitle'
|
||||
: 'home.chart.placeholder.subtitle'
|
||||
const VIEW_CHART_CONFIG: Record<
|
||||
Extract<
|
||||
HomeChartId,
|
||||
'blog_views_30d' | 'product_views_30d' | 'website_views_30d' | 'portfolio_views_30d'
|
||||
>,
|
||||
{
|
||||
kind: WebsiteViewKind
|
||||
titleKey: BusinessMessageKey
|
||||
subtitleKey: BusinessMessageKey
|
||||
legendKey: BusinessMessageKey
|
||||
loadingKey: BusinessMessageKey
|
||||
errorKey: BusinessMessageKey
|
||||
barKey: BusinessMessageKey
|
||||
}
|
||||
> = {
|
||||
product_views_30d: {
|
||||
kind: 'product',
|
||||
titleKey: 'home.chart.productViews.title',
|
||||
subtitleKey: 'home.chart.productViews.subtitle',
|
||||
legendKey: 'home.chart.productViews.legend',
|
||||
loadingKey: 'home.chart.productViews.loading',
|
||||
errorKey: 'home.chart.productViews.error',
|
||||
barKey: 'home.chart.productViews.bar',
|
||||
},
|
||||
website_views_30d: {
|
||||
kind: 'website',
|
||||
titleKey: 'home.chart.websiteViews.title',
|
||||
subtitleKey: 'home.chart.websiteViews.subtitle',
|
||||
legendKey: 'home.chart.websiteViews.legend',
|
||||
loadingKey: 'home.chart.websiteViews.loading',
|
||||
errorKey: 'home.chart.websiteViews.error',
|
||||
barKey: 'home.chart.websiteViews.bar',
|
||||
},
|
||||
portfolio_views_30d: {
|
||||
kind: 'portfolio',
|
||||
titleKey: 'home.chart.portfolioViews.title',
|
||||
subtitleKey: 'home.chart.portfolioViews.subtitle',
|
||||
legendKey: 'home.chart.portfolioViews.legend',
|
||||
loadingKey: 'home.chart.portfolioViews.loading',
|
||||
errorKey: 'home.chart.portfolioViews.error',
|
||||
barKey: 'home.chart.portfolioViews.bar',
|
||||
},
|
||||
blog_views_30d: {
|
||||
kind: 'blog',
|
||||
titleKey: 'home.chart.blogViews.title',
|
||||
subtitleKey: 'home.chart.blogViews.subtitle',
|
||||
legendKey: 'home.chart.blogViews.legend',
|
||||
loadingKey: 'home.chart.blogViews.loading',
|
||||
errorKey: 'home.chart.blogViews.error',
|
||||
barKey: 'home.chart.blogViews.bar',
|
||||
},
|
||||
}
|
||||
|
||||
function ViewActivityChart({
|
||||
chartId,
|
||||
}: {
|
||||
chartId: keyof typeof VIEW_CHART_CONFIG
|
||||
}) {
|
||||
const config = VIEW_CHART_CONFIG[chartId]
|
||||
const load = useCallback(
|
||||
(signal: AbortSignal) => getViewDailyActivity(config.kind, 30, signal),
|
||||
[config.kind],
|
||||
)
|
||||
|
||||
return (
|
||||
<section className={styles.card} aria-label={t(titleKey)}>
|
||||
<div className={styles.header}>
|
||||
<div>
|
||||
<h3 className={styles.title}>{t(titleKey)}</h3>
|
||||
<p className={styles.subtitle}>{t(subtitleKey)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className={styles.status}>{t('home.chart.placeholder.comingSoon')}</p>
|
||||
</section>
|
||||
<DailyActivityChart
|
||||
showSecondary={false}
|
||||
titleKey={config.titleKey}
|
||||
subtitleKey={config.subtitleKey}
|
||||
primaryLegendKey={config.legendKey}
|
||||
secondaryLegendKey={config.legendKey}
|
||||
loadingKey={config.loadingKey}
|
||||
errorKey={config.errorKey}
|
||||
primaryBarTitleKey={config.barKey}
|
||||
secondaryBarTitleKey={config.barKey}
|
||||
load={load}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -81,8 +138,11 @@ export function HomeChartSlot({ chartId }: HomeChartSlotProps) {
|
||||
)
|
||||
case 'products_added_1y':
|
||||
return <ProductActivityChart />
|
||||
case 'product_views_30d':
|
||||
case 'website_views_30d':
|
||||
case 'portfolio_views_30d':
|
||||
case 'blog_views_30d':
|
||||
return <HomeChartPlaceholder chartId={chartId} />
|
||||
return <ViewActivityChart chartId={chartId} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user