feat: enhance PDF export with top pages/referrers and move export button to page header

This commit is contained in:
Usman Baig
2026-01-30 14:16:26 +01:00
parent c01b042254
commit e19d72ebb8
3 changed files with 90 additions and 28 deletions

View File

@@ -11,8 +11,8 @@ import TopReferrers from '@/components/dashboard/TopReferrers'
import Locations from '@/components/dashboard/Locations'
import TechSpecs from '@/components/dashboard/TechSpecs'
import PerformanceStats from '@/components/dashboard/PerformanceStats'
import { Select, DatePicker as DatePickerModal, Captcha } from '@ciphera-net/ui'
import { ZapIcon } from '@ciphera-net/ui'
import { Select, DatePicker as DatePickerModal, Captcha, DownloadIcon, ZapIcon } from '@ciphera-net/ui'
import ExportModal from '@/components/dashboard/ExportModal'
// Helper to get date ranges
const getDateRange = (days: number) => {
@@ -45,6 +45,7 @@ export default function PublicDashboardPage() {
// Date range state
const [dateRange, setDateRange] = useState(getDateRange(30))
const [isDatePickerOpen, setIsDatePickerOpen] = useState(false)
const [isExportModalOpen, setIsExportModalOpen] = useState(false)
const [todayInterval, setTodayInterval] = useState<'minute' | 'hour'>('hour')
const [multiDayInterval, setMultiDayInterval] = useState<'hour' | 'day'>('day')
@@ -294,6 +295,14 @@ export default function PublicDashboardPage() {
</div>
<div className="flex gap-2">
<button
onClick={() => setIsExportModalOpen(true)}
className="hidden md:flex items-center gap-2 px-3 py-2 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-lg text-sm text-neutral-600 dark:text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange transition-colors"
>
<DownloadIcon className="w-4 h-4" />
<span>Export</span>
</button>
<Select
value={
dateRange.start === new Date().toISOString().split('T')[0] && dateRange.end === new Date().toISOString().split('T')[0]
@@ -454,6 +463,17 @@ export default function PublicDashboardPage() {
setIsDatePickerOpen(false)
}}
/>
{data && (
<ExportModal
isOpen={isExportModalOpen}
onClose={() => setIsExportModalOpen(false)}
data={data.daily_stats || []}
stats={data.stats}
topPages={data.top_pages}
topReferrers={data.top_referrers}
/>
)}
</div>
)
}

View File

@@ -14,10 +14,8 @@ import {
} from 'recharts'
import type { TooltipProps } from 'recharts'
import { formatNumber, formatDuration } from '@/lib/utils/format'
import { ArrowUpRightIcon, ArrowDownRightIcon, DownloadIcon, BarChartIcon } from '@ciphera-net/ui'
import { Button } from '@ciphera-net/ui'
import { ArrowUpRightIcon, ArrowDownRightIcon, BarChartIcon } from '@ciphera-net/ui'
import { Checkbox } from '@ciphera-net/ui'
import ExportModal from './ExportModal'
const COLORS = {
brand: '#FD5E0F',
@@ -164,7 +162,6 @@ function formatAxisValue(value: number): string {
export default function Chart({ data, prevData, stats, prevStats, interval }: ChartProps) {
const [metric, setMetric] = useState<MetricType>('visitors')
const [showComparison, setShowComparison] = useState(false)
const [isExportModalOpen, setIsExportModalOpen] = useState(false)
const { resolvedTheme } = useTheme()
const colors = useMemo(
@@ -214,10 +211,6 @@ export default function Chart({ data, prevData, stats, prevStats, interval }: Ch
return Math.round(((current - previous) / previous) * 100)
}
const handleExport = () => {
setIsExportModalOpen(true)
}
const metrics = [
{
id: 'visitors',
@@ -364,16 +357,6 @@ export default function Chart({ data, prevData, stats, prevStats, interval }: Ch
{/* Vertical Separator */}
<div className="h-4 w-px bg-neutral-200 dark:bg-neutral-800" />
<Button
variant="secondary"
onClick={handleExport}
className="h-8 px-3 text-xs gap-2"
title="Export to CSV"
>
<DownloadIcon className="w-3.5 h-3.5" />
Export
</Button>
</div>
</div>
@@ -494,12 +477,6 @@ export default function Chart({ data, prevData, stats, prevStats, interval }: Ch
</div>
)}
</div>
<ExportModal
isOpen={isExportModalOpen}
onClose={() => setIsExportModalOpen(false)}
data={data}
stats={stats}
/>
</div>
)
}

View File

@@ -7,6 +7,7 @@ import jsPDF from 'jspdf'
import autoTable from 'jspdf-autotable'
import type { DailyStat } from './Chart'
import { formatNumber, formatDuration } from '@/lib/utils/format'
import type { TopPage, TopReferrer } from '@/lib/api/stats'
interface ExportModalProps {
isOpen: boolean
@@ -18,6 +19,8 @@ interface ExportModalProps {
bounce_rate: number
avg_duration: number
}
topPages?: TopPage[]
topReferrers?: TopReferrer[]
}
type ExportFormat = 'csv' | 'json' | 'xlsx' | 'pdf'
@@ -40,7 +43,7 @@ const loadImage = (src: string): Promise<string> => {
})
}
export default function ExportModal({ isOpen, onClose, data, stats }: ExportModalProps) {
export default function ExportModal({ isOpen, onClose, data, stats, topPages, topReferrers }: ExportModalProps) {
const [format, setFormat] = useState<ExportFormat>('csv')
const [filename, setFilename] = useState(`pulse_export_${new Date().toISOString().split('T')[0]}`)
const [includeHeader, setIncludeHeader] = useState(true)
@@ -171,11 +174,17 @@ export default function ExportModal({ isOpen, onClose, data, stats }: ExportModa
startY = 65 // Move table down
}
// Check if data is hourly (same date for multiple rows)
const isHourly = data.length > 1 && data[0].date.split('T')[0] === data[1].date.split('T')[0]
const tableData = exportData.map(row =>
fields.map(field => {
const val = row[field]
if (field === 'date' && typeof val === 'string') {
return new Date(val).toLocaleDateString()
const date = new Date(val)
return isHourly
? date.toLocaleString('en-US', { month: 'numeric', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' })
: date.toLocaleDateString()
}
if (typeof val === 'number') {
if (field === 'bounce_rate') return `${Math.round(val)}%`
@@ -226,6 +235,62 @@ export default function ExportModal({ isOpen, onClose, data, stats }: ExportModa
}
})
let finalY = (doc as any).lastAutoTable.finalY + 10
// Top Pages Table
if (topPages && topPages.length > 0) {
// Check if we need a new page
if (finalY + 40 > doc.internal.pageSize.height) {
doc.addPage()
finalY = 20
}
doc.setFontSize(14)
doc.setTextColor(23, 23, 23)
doc.text('Top Pages', 14, finalY)
finalY += 5
const pagesData = topPages.slice(0, 10).map(p => [p.path, formatNumber(p.pageviews)])
autoTable(doc, {
startY: finalY,
head: [['Path', 'Pageviews']],
body: pagesData,
styles: { font: 'helvetica', fontSize: 9, cellPadding: 3 },
headStyles: { fillColor: [249, 115, 22], textColor: [255, 255, 255], fontStyle: 'bold' },
columnStyles: { 1: { halign: 'right' } },
alternateRowStyles: { fillColor: [255, 250, 245] },
})
finalY = (doc as any).lastAutoTable.finalY + 10
}
// Top Referrers Table
if (topReferrers && topReferrers.length > 0) {
// Check if we need a new page
if (finalY + 40 > doc.internal.pageSize.height) {
doc.addPage()
finalY = 20
}
doc.setFontSize(14)
doc.setTextColor(23, 23, 23)
doc.text('Top Referrers', 14, finalY)
finalY += 5
const referrersData = topReferrers.slice(0, 10).map(r => [r.referrer, formatNumber(r.pageviews)])
autoTable(doc, {
startY: finalY,
head: [['Referrer', 'Pageviews']],
body: referrersData,
styles: { font: 'helvetica', fontSize: 9, cellPadding: 3 },
headStyles: { fillColor: [249, 115, 22], textColor: [255, 255, 255], fontStyle: 'bold' },
columnStyles: { 1: { halign: 'right' } },
alternateRowStyles: { fillColor: [255, 250, 245] },
})
}
doc.save(`${filename || 'export'}.pdf`)
onClose()
return