diff --git a/app/lish/[did]/[publication]/dashboard/PublicationAnalytics.tsx b/app/lish/[did]/[publication]/dashboard/PublicationAnalytics.tsx index 7ed5acb6..b1c437f3 100644 --- a/app/lish/[did]/[publication]/dashboard/PublicationAnalytics.tsx +++ b/app/lish/[did]/[publication]/dashboard/PublicationAnalytics.tsx @@ -2,9 +2,10 @@ import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; import { UpgradeContent } from "../UpgradeModal"; import { Popover } from "components/Popover"; import { DatePicker } from "components/DatePicker"; -import { Fragment, useMemo, useState } from "react"; +import { Fragment, useCallback, useMemo, useState } from "react"; import { useLocalizedDate } from "src/hooks/useLocalizedDate"; import type { DateRange } from "react-day-picker"; +import { useQueryState } from "src/hooks/useQueryState"; import { usePublicationData } from "./PublicationSWRProvider"; import { Combobox, ComboboxResult } from "components/Combobox"; import { useIsPro, useCanSeePro } from "src/hooks/useEntitlement"; @@ -22,6 +23,8 @@ import { type ReferrerType = { referrer_host: string; pageviews: number }; type TrafficMetric = "pageviews" | "visitors"; +type DatePreset = "Last Week" | "Last Month" | "All Time"; +type DateSelection = { range: DateRange; preset: DatePreset | null }; const dayTickFormatter = new Intl.DateTimeFormat(undefined, { month: "short", @@ -89,20 +92,94 @@ export const PublicationAnalytics = (props: { let canSeePro = useCanSeePro(); let { data: publication } = usePublicationData(); - let [dateRange, setDateRange] = useState(() => { - let from = new Date(); - from.setDate(from.getDate() - 7); - return { from, to: new Date() }; + + // URL params: + // ?date=all → All Time (no from/to bounds) + // ?date=month → Last Month preset + // ?from=YYYY-MM-DD&to=YYYY-MM-DD → custom range + // (none) → default: Last Week + let [dateState, setDateState] = useQueryState({ + fromParams: (get) => { + let preset = get("date"); + if (preset === "all") + return { + range: { from: undefined, to: undefined }, + preset: "All Time", + }; + if (preset === "month") { + let from = new Date(); + from.setMonth(from.getMonth() - 1); + return { range: { from, to: new Date() }, preset: "Last Month" }; + } + let fromParam = get("from"); + let toParam = get("to"); + if (fromParam || toParam) { + return { + range: { + from: fromParam ? new Date(fromParam) : undefined, + to: toParam ? new Date(toParam) : new Date(), + }, + preset: null, + }; + } + let from = new Date(); + from.setDate(from.getDate() - 7); + return { range: { from, to: new Date() }, preset: "Last Week" }; + }, + toParams: ({ preset, range }) => { + if (preset === "All Time") + return { date: "all", from: null, to: null }; + if (preset === "Last Month") + return { date: "month", from: null, to: null }; + if (preset === "Last Week") return { date: null, from: null, to: null }; + return { + date: null, + from: range.from?.toISOString().slice(0, 10) ?? null, + to: range.to?.toISOString().slice(0, 10) ?? null, + }; + }, + }); + let { range: dateRange, preset: datePreset } = dateState; + + // ?post= — filter traffic to a single published post (path is the stable ID) + let [selectedPostPath, setSelectedPostPath] = useQueryState< + string | undefined + >({ + fromParams: (get) => get("post") ?? undefined, + toParams: (v) => ({ post: v ?? null }), + }); + let documents = publication?.documents; + let selectedPost = useMemo(() => { + if (!selectedPostPath) return undefined; + let doc = documents?.find( + (d) => (d.record.path || "") === selectedPostPath, + ); + return { + title: doc?.record.title || selectedPostPath, + path: selectedPostPath, + }; + }, [selectedPostPath, documents]); + let setSelectedPost = useCallback( + (post: { title: string; path: string } | undefined) => { + setSelectedPostPath(post?.path); + }, + [setSelectedPostPath], + ); + + // ?referrer= — filter traffic to a single referring domain + let [selectedReferrer, setSelectedReferrer] = useQueryState< + string | undefined + >({ + fromParams: (get) => get("referrer") ?? undefined, + toParams: (v) => ({ referrer: v ?? null }), + }); + + // ?metric=pageviews — toggle between "visitors" (default) and "pageviews" + let [trafficMetric, setTrafficMetric] = useQueryState({ + fromParams: (get) => + get("metric") === "pageviews" ? "pageviews" : "visitors", + toParams: (v) => ({ metric: v === "visitors" ? null : v }), }); - let [datePreset, setDatePreset] = useState("Last Week"); - let [selectedPost, setSelectedPost] = useState< - { title: string; path: string } | undefined - >(undefined); - let [selectedReferror, setSelectedReferror] = useState< - ReferrerType | undefined - >(undefined); - let [trafficMetric, setTrafficMetric] = - useState("visitors"); let publicationUri = publication?.publication?.uri; @@ -114,7 +191,7 @@ export const PublicationAnalytics = (props: { dateRange.from?.toISOString(), dateRange.to?.toISOString(), selectedPost?.path, - selectedReferror?.referrer_host, + selectedReferrer, ] : null, async () => { @@ -123,9 +200,7 @@ export const PublicationAnalytics = (props: { ...(dateRange.from ? { from: dateRange.from.toISOString() } : {}), ...(dateRange.to ? { to: endOfDay(dateRange.to).toISOString() } : {}), ...(selectedPost ? { path: `/${selectedPost.path}` } : {}), - ...(selectedReferror - ? { referrer_host: selectedReferror.referrer_host } - : {}), + ...(selectedReferrer ? { referrer_host: selectedReferrer } : {}), }); return res?.result; }, @@ -185,10 +260,8 @@ export const PublicationAnalytics = (props: {
@@ -213,12 +286,12 @@ export const PublicationAnalytics = (props: { selectedPost={selectedPost} setSelectedPost={setSelectedPost} /> - {selectedReferror && ( + {selectedReferrer && ( <>
{" "} - {selectedReferror.referrer_host} + {selectedReferrer}
)} @@ -233,8 +306,8 @@ export const PublicationAnalytics = (props: { />
@@ -569,19 +642,18 @@ const PostSelector = (props: { const DateRangeSelector = (props: { pubStartDate: string | undefined; - dateRange: DateRange; - setDateRange: (dateRange: DateRange) => void; - datePreset: string | null; - setDatePreset: (preset: string | null) => void; + dateState: DateSelection; + setDateState: (state: DateSelection) => void; showBackground?: boolean; }) => { + let { range: dateRange, preset: datePreset } = props.dateState; let buttonClass = "rounded-md px-1 text-sm border border-accent-contrast text-accent-contrast"; let currentDate = new Date(); let startDate = useLocalizedDate( - props.dateRange.from?.toISOString() || + dateRange.from?.toISOString() || props.pubStartDate || "2025-01-01T12:00:00.000Z", { @@ -591,20 +663,13 @@ const DateRangeSelector = (props: { ); let endDate = useLocalizedDate( - (props.dateRange.to ?? currentDate).toISOString(), + (dateRange.to ?? currentDate).toISOString(), { month: "short", day: "numeric", }, ); - function handleDateChange(range: DateRange | undefined) { - if (range) { - props.setDateRange(range); - props.setDatePreset(null); - } - } - return ( - {props.datePreset - ? props.datePreset - : props.dateRange.from === undefined + {datePreset + ? datePreset + : dateRange.from === undefined ? "All Time" : `${startDate} - ${endDate}`}
@@ -622,10 +687,12 @@ const DateRangeSelector = (props: { >