import { useEffect, useId, useRef, useState, type CSSProperties } from "react"; import { applyLayoutPreference, dashboardLayout, layoutPreference, packLayout, panelNavItem, panelStyle, renderableChartType, setPanelVisible, visiblePanels, type DashboardLayout, type DashboardPanel, type DashboardSectionKey, } from "./dashboard-layout"; import { DashboardEditor } from "./dashboard-editor"; import { clearDashboardLayout, dashboardLayoutKey, readDashboardLayout, writeDashboardLayout, type DashboardLayoutStore, } from "./dashboard-storage"; import { CauseWiseFunnel } from "./funnel"; import { formatDate, formatRange } from "./format"; import { CauseWiseGoals } from "./goals"; import { CauseWiseRecentEvents } from "./recent-events"; import { CauseWiseReferrers } from "./referrers"; import { sectionInRoot, useCurrentNavSection } from "./nav-section"; import { Root } from "./section"; import { CauseWiseStats } from "./stats"; import { CauseWiseTopPages } from "./top-pages"; import { CauseWiseTrafficChart } from "./traffic-chart"; import type { DashboardSections, FunnelsSection, FunnelData, GoalsSection, GoalRow, OverviewStats, RecentEvent, Referrer, ReportingPeriod, StatusNotice, ThemeName, TopPage, TrafficPoint, TrafficSeries, ViewState, } from "./types"; const RANGE_OPTIONS = [ { id: "today", label: "Today" }, { id: "7d", label: "Last 7 days" }, { id: "30d", label: "Last 30 days" }, ] as const; export type CauseWiseDashboardProps = { title?: string; subtitle?: string; period?: ReportingPeriod | null; stats?: OverviewStats | null; trend?: TrafficSeries | TrafficPoint[] | null; topPages?: TopPage[] | null; referrers?: Referrer[] | null; goals?: GoalsSection | GoalRow[] | null; funnels?: FunnelsSection | FunnelData[] | null; recentEvents?: RecentEvent[] | null; sections?: DashboardSections; /** Fixed layout for this release. Visibility and range can override the default. */ layout?: DashboardLayout; /** Section flags or a comma-separated allowlist. Ignored when `layout` is passed. */ sectionVisibility?: Partial> | string; range?: string; onRangeChange?: (range: "today" | "7d" | "30d") => void; advancedHref?: string; notice?: string | null; theme?: ThemeName; state?: ViewState; error?: StatusNotice | null; className?: string; /** * Enables Customize layout for one authorized viewer. * `layoutStore` is the account preference. `storage` is only the temporary browser copy. * Pass `storage: null` when localStorage cannot be used. The edit still works for this visit. */ layoutScope?: { site: string; userId: string | number; instance: string; storage?: Storage | null; layoutStore?: DashboardLayoutStore; }; }; function shown(flag: boolean | undefined, present: boolean) { if (flag === false) return false; if (flag === true) return true; return present; } function safeAdvancedHref(url: string | undefined) { try { const parsed = new URL(url ?? ""); if (parsed.protocol === "https:" && parsed.hostname === "causewise.io") return parsed.toString(); } catch { /* Fall through to the public workspace. */ } return "https://causewise.io/dashboard"; } export function CauseWiseDashboard(props: CauseWiseDashboardProps) { if (props.layout || props.sectionVisibility !== undefined || props.onRangeChange) { const layout = props.layout ?? dashboardLayout({ range: props.range, sections: props.sectionVisibility, }); return ; } return ; } function StackedDashboard({ title = "Analytics", period, stats, trend, topPages, goals, funnels, recentEvents, sections, theme = "system", state = "ready", error, className, }: CauseWiseDashboardProps) { const funnelList = Array.isArray(funnels) ? funnels : funnels?.available ? funnels.funnels : []; const funnelUnavailable = !Array.isArray(funnels) && funnels?.available === false; const showPeriod = shown(sections?.period, period != null); const showStats = shown(sections?.stats, stats != null); const showTraffic = shown(sections?.traffic, trend != null); const showPages = shown(sections?.topPages, topPages != null); const showGoals = shown(sections?.goals, goals != null); const showFunnels = shown(sections?.funnels, funnels != null); const showEvents = shown(sections?.recentEvents, recentEvents != null); return (

{title}

{state === "ready" && showPeriod && period ? (

{formatRange(period.range)} {period.start && period.end ? `, ${formatDate(period.start)} to ${formatDate(period.end)}` : ""} {period.retentionDays != null ? `. Retention ${period.retentionDays} days.` : ""}

) : null} {state === "ready" && showPeriod && !period ? (

No reporting period was included.

) : null}
{state === "loading" ?

Loading

: null} {state === "error" ? (

{error?.message ?? "Analytics could not be displayed."}

) : null} {state === "ready" ? (
{showStats ? : null} {showTraffic ? : null} {showPages ? : null} {showGoals ? : null} {showFunnels && funnelUnavailable && !Array.isArray(funnels) ? (

Funnels

{funnels?.reason}

) : null} {showFunnels && !funnelUnavailable ? funnelList.map((funnel) => ( )) : null} {showFunnels && !funnelUnavailable && funnelList.length === 0 ? (

Funnels

No funnels were included.

) : null} {!Array.isArray(funnels) && funnels?.available && funnels.truncated ? (

The funnel list is truncated.

) : null} {showEvents ? : null}
) : null}
); } function LayoutDashboard({ title = "Analytics", subtitle, period, stats, trend, topPages, referrers, goals, funnels, recentEvents, layout, range, onRangeChange, advancedHref, notice, theme = "system", state = "ready", error, className, layoutScope, }: CauseWiseDashboardProps & { layout: DashboardLayout }) { const prefix = useId().replace(/:/g, ""); const storageKey = layoutScope ? dashboardLayoutKey(layoutScope.site, String(layoutScope.userId), layoutScope.instance) : ""; const storage = layoutScope ? resolveLayoutStorage(layoutScope.storage) : null; const layoutStore = layoutScope?.layoutStore; const authorSignature = layout.panels.map((item) => `${item.id}:${item.visible ? 1 : 0}:${item.w}`).join("|"); const authorLayout = useRef(layout); authorLayout.current = layout; const [viewerLayout, setViewerLayout] = useState(() => layoutStore ? packLayout(layout) : savedViewerLayout(layout, storage, storageKey), ); const [editing, setEditing] = useState(false); const [layoutReady, setLayoutReady] = useState(!layoutStore); const [layoutMessage, setLayoutMessage] = useState(""); const edited = useRef(false); const generation = useRef(0); const pendingLayout = useRef(null); const saveTimer = useRef | null>(null); const inflight = useRef | null>(null); const persistRef = useRef<(next: DashboardLayout, gen: number) => Promise>(async () => undefined); persistRef.current = async (next, gen) => { if (!layoutStore || gen !== generation.current) return; const run = (async () => { try { await layoutStore.save(layoutPreference(next)); if (gen !== generation.current) return; clearDashboardLayout(storage, storageKey); } catch { if (gen !== generation.current) return; const kept = writeDashboardLayout(storage, storageKey, next); setLayoutMessage( kept ? "The layout could not be saved to WordPress. This browser will keep it for now." : "This browser could not save the layout. The change lasts for this visit.", ); } })(); inflight.current = run; await run; }; function flushSave() { if (saveTimer.current) clearTimeout(saveTimer.current); saveTimer.current = null; const next = pendingLayout.current; pendingLayout.current = null; if (next) void persistRef.current(next, generation.current); } const flushRef = useRef(flushSave); flushRef.current = flushSave; useEffect(() => () => flushRef.current(), []); useEffect(() => { if (!storageKey) return; if (!layoutStore) { setViewerLayout(savedViewerLayout(authorLayout.current, storage, storageKey)); setLayoutReady(true); return; } let cancelled = false; setLayoutReady(false); layoutStore .load() .then((saved) => { if (cancelled || edited.current) return; clearDashboardLayout(storage, storageKey); setViewerLayout( saved ? applyLayoutPreference(authorLayout.current, saved) : packLayout(authorLayout.current), ); setLayoutMessage(""); }) .catch(() => { if (cancelled || edited.current) return; const local = readDashboardLayout(storage, storageKey); if (local) { setViewerLayout(applyLayoutPreference(authorLayout.current, local)); setLayoutMessage("The saved layout could not be loaded from WordPress. Showing the copy from this browser."); } else { setViewerLayout(packLayout(authorLayout.current)); setLayoutMessage("The saved layout could not be loaded from WordPress."); } }) .finally(() => { if (!cancelled) setLayoutReady(true); }); return () => { cancelled = true; }; }, [authorSignature, layoutStore, storage, storageKey]); const displayed = storageKey ? viewerLayout : layout; const panels = visiblePanels(displayed); const navRef = useRef(null); const sectionIds = panels.map((panelItem) => `${prefix}-${panelItem.id}`); const { active: activeSection, activate } = useCurrentNavSection(navRef, sectionIds, () => 8, authorSignature); const selected = range === "today" || range === "30d" || range === "7d" ? range : layout.range; const funnelList = Array.isArray(funnels) ? funnels : funnels?.available ? funnels.funnels : []; const goalSection = Array.isArray(goals) ? { available: true as const, goals } : goals; function panelId(panelItem: DashboardPanel) { return `${prefix}-${panelItem.id}`; } function updateLayout(next: DashboardLayout, message: string) { const packed = packLayout(next); edited.current = true; setViewerLayout(packed); setLayoutMessage(message); if (!storageKey) return; if (!layoutStore) { if (!writeDashboardLayout(storage, storageKey, packed)) { setLayoutMessage("This browser could not save the layout. The change lasts for this visit."); } return; } pendingLayout.current = packed; if (saveTimer.current) clearTimeout(saveTimer.current); saveTimer.current = setTimeout(() => { saveTimer.current = null; const queued = pendingLayout.current; pendingLayout.current = null; if (queued) void persistRef.current(queued, generation.current); }, 300); } return (

{title}

{subtitle ?

{subtitle}

: null} {state === "ready" && period?.start && period.end ? (

{formatRange(period.range)}, {formatDate(period.start)} to {formatDate(period.end)}

) : null}
{RANGE_OPTIONS.map((item) => ( ))}
Advanced analytics {storageKey && layoutReady ? (
{editing ? ( ) : null}
) : null}
{layoutMessage ? (

{layoutMessage}

) : null} {panels.length > 0 ? ( ) : (

Choose a dashboard section.

)} {state === "error" ? (

{error?.message ?? "Analytics could not be displayed."}

) : null} {state === "ready" && notice ? (

{notice}

) : null} {state !== "error" && panels.length > 0 ? ( editing ? ( updateLayout(setPanelVisible(displayed, panelItem.id, false), `${panelNavItem(panelItem.type).label} hidden.`)} panelDomId={panelId} renderPanel={(panelItem) => renderPanel(panelItem, { state, stats, trend, topPages, referrers, goalSection, funnelList, funnels, recentEvents, error, }) } /> ) : (
{panels.map((panelItem) => (
{renderPanel(panelItem, { state, stats, trend, topPages, referrers, goalSection, funnelList, funnels, recentEvents, error, })}
))}
) ) : null}
); } function resolveLayoutStorage(storage: Storage | null | undefined): Storage | null { if (storage === null) return null; if (storage) return storage; try { return window.localStorage; } catch { return null; } } function savedViewerLayout(author: DashboardLayout, storage: Storage | null, key: string): DashboardLayout { if (!key) return packLayout(author); return applyLayoutPreference(author, readDashboardLayout(storage, key)); } function renderPanel( panelItem: DashboardPanel, data: { state: ViewState; stats?: OverviewStats | null; trend?: TrafficSeries | TrafficPoint[] | null; topPages?: TopPage[] | null; referrers?: Referrer[] | null; goalSection?: GoalsSection | null; funnelList: FunnelData[]; funnels?: FunnelsSection | FunnelData[] | null; recentEvents?: RecentEvent[] | null; error?: StatusNotice | null; }, ) { const view = data.state === "loading" ? "loading" : "ready"; if (panelItem.type === "stats") { return ; } if (panelItem.type === "traffic") { const points = Array.isArray(data.trend) ? data.trend : data.trend?.trend; if (view === "ready" && points && points.length === 0) { return (

Traffic

No traffic in this period.

); } const trafficState = view === "loading" ? "loading" : data.trend ? "ready" : "error"; return ( ); } if (panelItem.type === "top-pages") { return ; } if (panelItem.type === "referrers") { return ( ); } if (panelItem.type === "goals") { if (view === "ready" && data.goalSection?.available === true && data.goalSection.goals.length === 0) { return (

Goals

No goals have been configured yet.

); } return ; } if (panelItem.type === "funnels") { if (view === "loading") { return (

Funnels

Loading

); } if (!Array.isArray(data.funnels) && data.funnels?.available === false) { return (

Funnels

{data.funnels.reason}

); } if (data.funnelList.length === 0) { return (

Funnels

No funnels have been configured yet.

); } return (
{data.funnelList.map((funnel) => ( ))} {!Array.isArray(data.funnels) && data.funnels?.available && data.funnels.truncated ? (

The funnel list is truncated.

) : null}
); } return ; }