Added download functionality.

This commit is contained in:
Mike Cao 2025-07-22 00:24:37 -07:00
parent 0debe89d05
commit 7670ec4136
17 changed files with 216 additions and 5 deletions

View file

@ -1,6 +1,7 @@
import { useContext } from 'react';
import { ReportContext } from './Report';
import styles from './ReportBody.module.css';
import { DownloadButton } from '@/components/input/DownloadButton';
export function ReportBody({ children }) {
const { report } = useContext(ReportContext);
@ -9,7 +10,12 @@ export function ReportBody({ children }) {
return null;
}
return <div className={styles.body}>{children}</div>;
return (
<div className={styles.body}>
<DownloadButton filename={report.name} data={report.data} />
{children}
</div>
);
}
export default ReportBody;

View file

@ -31,5 +31,9 @@ export default function ReportPage({ reportId }: { reportId: string }) {
const ReportComponent = reports[report.type];
if (!ReportComponent) {
return null;
}
return <ReportComponent reportId={reportId} />;
}

View file

@ -1,6 +1,6 @@
import { Dropdown, Item } from 'react-basics';
import classNames from 'classnames';
import { useDateRange, useMessages, useSticky } from '@/components/hooks';
import { useDateRange, useMessages, useNavigation, useSticky } from '@/components/hooks';
import WebsiteDateFilter from '@/components/input/WebsiteDateFilter';
import MetricCard from '@/components/metrics/MetricCard';
import MetricsBar from '@/components/metrics/MetricsBar';
@ -8,6 +8,7 @@ import { formatShortTime, formatLongNumber } from '@/lib/format';
import useWebsiteStats from '@/components/hooks/queries/useWebsiteStats';
import useStore, { setWebsiteDateCompare } from '@/store/websites';
import WebsiteFilterButton from './WebsiteFilterButton';
import { ExportButton } from '@/components/input/ExportButton';
import styles from './WebsiteMetricsBar.module.css';
export function WebsiteMetricsBar({
@ -31,6 +32,9 @@ export function WebsiteMetricsBar({
websiteId,
compareMode && dateCompare,
);
const {
query: { view },
} = useNavigation();
const isAllTime = dateRange.value === 'all';
const { pageviews, visitors, visits, bounces, totaltime } = data || {};
@ -109,7 +113,10 @@ export function WebsiteMetricsBar({
</MetricsBar>
</div>
<div className={styles.actions}>
{showFilter && <WebsiteFilterButton websiteId={websiteId} />}
<div>
{showFilter && <WebsiteFilterButton websiteId={websiteId} />}
{!view && <ExportButton websiteId={websiteId} />}
</div>
<WebsiteDateFilter websiteId={websiteId} showAllTime={!compareMode} />
{compareMode && (
<div className={styles.vs}>

View file

@ -14,6 +14,7 @@ export default function WebsiteTableView({ websiteId }: { websiteId: string }) {
const pathname = usePathname();
const tableProps = {
websiteId,
allowDownload: false,
limit: 10,
};
const isSharePage = pathname.includes('/share/');

View file

@ -0,0 +1,75 @@
import { z } from 'zod';
import JSZip from 'jszip';
import Papa from 'papaparse';
import { getRequestFilters, parseRequest } from '@/lib/request';
import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth';
import { pagingParams } from '@/lib/schema';
import { getEventMetrics, getWebsiteEvents } from '@/queries';
export async function GET(
request: Request,
{ params }: { params: Promise<{ websiteId: string }> },
) {
const schema = z.object({
startAt: z.coerce.number().int(),
endAt: z.coerce.number().int(),
...pagingParams,
});
const { auth, query, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const { websiteId } = await params;
const { startAt, endAt } = query;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const startDate = new Date(+startAt);
const endDate = new Date(+endAt);
const limit = 10;
const offset = 0;
const filters = {
...(await getRequestFilters(query)),
startDate,
endDate,
};
const [events, pages, referrers, browsers, os, devices, countries] = await Promise.all([
getWebsiteEvents(websiteId, { startDate, endDate }, query),
getEventMetrics(websiteId, 'url', filters, limit, offset),
getEventMetrics(websiteId, 'referrer', filters, limit, offset),
getEventMetrics(websiteId, 'browser', filters, limit, offset),
getEventMetrics(websiteId, 'os', filters, limit, offset),
getEventMetrics(websiteId, 'device', filters, limit, offset),
getEventMetrics(websiteId, 'country', filters, limit, offset),
]);
const zip = new JSZip();
const parse = (data: any) => {
return Papa.unparse(data, {
header: true,
skipEmptyLines: true,
});
};
zip.file('events.csv', parse(events?.data));
zip.file('pages.csv', parse(pages));
zip.file('referrers.csv', parse(referrers));
zip.file('browsers.csv', parse(browsers));
zip.file('os.csv', parse(os));
zip.file('devices.csv', parse(devices));
zip.file('countries.csv', parse(countries));
const content = await zip.generateAsync({ type: 'nodebuffer' });
const base64 = content.toString('base64');
return json({ zip: base64 });
}