Add a Statistics page for storefront visits and instruction 1:1/3:2 crops.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
d9bdfbbb12
commit
6be867ea7a
@@ -0,0 +1,161 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Breadcrumbs } from '../components/Breadcrumbs'
|
||||
import { HomeChartSlot } from '../components/HomeChartSlot'
|
||||
import { PageTitle } from '../components/PageTitle'
|
||||
import { useTenantBranding } from '../context/TenantBrandingContext'
|
||||
import { useLocale } from '@meshkee/dashboard-ui'
|
||||
import { useT } from '../i18n/useT'
|
||||
import { translate, type BusinessMessageKey } from '../i18n/messages'
|
||||
import { ApiError } from '../lib/api'
|
||||
import {
|
||||
getViewSummary,
|
||||
type ViewPeriodCounts,
|
||||
type ViewSummaryGroupId,
|
||||
} from '../services/dailyActivityService'
|
||||
import {
|
||||
hasAnyBusinessModule,
|
||||
hasBusinessModule,
|
||||
isActiveHomeChart,
|
||||
type BusinessModuleId,
|
||||
} from '../utils/businessModules'
|
||||
import pageStyles from '../components/PageContent.module.css'
|
||||
import styles from './StatisticsPage.module.css'
|
||||
|
||||
const EMPTY_COUNTS: ViewPeriodCounts = {
|
||||
today: 0,
|
||||
week: 0,
|
||||
month: 0,
|
||||
total: 0,
|
||||
}
|
||||
|
||||
const METRICS: {
|
||||
id: keyof ViewPeriodCounts
|
||||
labelKey: BusinessMessageKey
|
||||
}[] = [
|
||||
{ id: 'today', labelKey: 'statistics.metric.today' },
|
||||
{ id: 'week', labelKey: 'statistics.metric.week' },
|
||||
{ id: 'month', labelKey: 'statistics.metric.month' },
|
||||
{ id: 'total', labelKey: 'statistics.metric.total' },
|
||||
]
|
||||
|
||||
const STAT_ROWS: {
|
||||
id: ViewSummaryGroupId
|
||||
titleKey: BusinessMessageKey
|
||||
moduleId?: BusinessModuleId
|
||||
anyModuleIds?: BusinessModuleId[]
|
||||
}[] = [
|
||||
{ id: 'all', titleKey: 'statistics.row.all' },
|
||||
{ id: 'home', titleKey: 'statistics.row.home' },
|
||||
{
|
||||
id: 'products',
|
||||
titleKey: 'statistics.row.products',
|
||||
anyModuleIds: ['products', 'store'],
|
||||
},
|
||||
{ id: 'blog', titleKey: 'statistics.row.blog', moduleId: 'blog' },
|
||||
{ id: 'portfolio', titleKey: 'statistics.row.portfolio', moduleId: 'portfolio' },
|
||||
{ id: 'videos', titleKey: 'statistics.row.videos', moduleId: 'videos' },
|
||||
]
|
||||
|
||||
function formatCount(value: number) {
|
||||
return new Intl.NumberFormat('en-US').format(value)
|
||||
}
|
||||
|
||||
export function StatisticsPage() {
|
||||
const t = useT()
|
||||
const { locale } = useLocale()
|
||||
const { enabledModules, homeCharts } = useTenantBranding()
|
||||
const [rows, setRows] = useState<Record<ViewSummaryGroupId, ViewPeriodCounts> | null>(
|
||||
null,
|
||||
)
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const visibleCharts = homeCharts.filter(isActiveHomeChart)
|
||||
const visibleRows = STAT_ROWS.filter((row) => {
|
||||
if (row.anyModuleIds) {
|
||||
return hasAnyBusinessModule(enabledModules, row.anyModuleIds)
|
||||
}
|
||||
return !row.moduleId || hasBusinessModule(enabledModules, row.moduleId)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
|
||||
async function load() {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
const data = await getViewSummary(controller.signal)
|
||||
setRows(data.rows)
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === 'AbortError') return
|
||||
setError(err instanceof ApiError ? err.message : t('statistics.errorLoad'))
|
||||
} finally {
|
||||
if (!controller.signal.aborted) setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
void load()
|
||||
return () => controller.abort()
|
||||
}, [t])
|
||||
|
||||
return (
|
||||
<main className={pageStyles.content}>
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ label: t('nav.home'), href: '/' },
|
||||
{ label: t('title.statistics') },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className={pageStyles.pageHeader}>
|
||||
<div>
|
||||
<PageTitle en="STATISTICS">{t('title.statistics')}</PageTitle>
|
||||
<p className={pageStyles.pageSubtitle}>{t('statistics.page.subtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{visibleCharts.length > 0 ? (
|
||||
<div className={pageStyles.grid12}>
|
||||
{visibleCharts.map((chartId, index) => (
|
||||
<div key={`${chartId}-${index}`} className={pageStyles.col6}>
|
||||
<HomeChartSlot chartId={chartId} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<div className={styles.error} role="alert">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{visibleRows.map((row) => {
|
||||
const counts = rows?.[row.id] ?? EMPTY_COUNTS
|
||||
return (
|
||||
<article key={row.id} className={styles.row}>
|
||||
<div className={styles.rowName}>
|
||||
<h3 className={styles.rowTitle}>{t(row.titleKey)}</h3>
|
||||
{locale === 'fa' ? (
|
||||
<span className={styles.rowTitleEn} lang="en" dir="ltr">
|
||||
{translate('en', row.titleKey)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className={styles.chips}>
|
||||
{METRICS.map((metric) => (
|
||||
<div key={metric.id} className={styles.chip}>
|
||||
<strong className={styles.chipValue} lang="en" dir="ltr">
|
||||
{loading || !rows ? '—' : formatCount(counts[metric.id])}
|
||||
</strong>
|
||||
<span className={styles.chipLabel}>{t(metric.labelKey)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
})}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user