Add plausible

This commit is contained in:
Andras Schmelczer 2026-02-22 23:14:42 +00:00
parent 48f2c97487
commit 4857800fca
14 changed files with 118 additions and 6 deletions

View file

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { useFadeInRef } from '../../hooks/useFadeIn';
import HexCanvas from './HexCanvas';
import ScrollStory from './ScrollStory';
@ -6,6 +6,7 @@ import BottomIllustration from './BottomIllustration';
import { TickerValue } from '../ui/TickerValue';
import { ChevronIcon } from '../ui/icons/ChevronIcon';
import { LogoIcon } from '../ui/icons/LogoIcon';
import { trackEvent } from '../../lib/analytics';
import type { FeatureMeta } from '../../types';
export default function HomePage({
@ -30,6 +31,35 @@ export default function HomePage({
const whyRef = useFadeInRef();
const ctaRef = useFadeInRef();
// Scroll depth tracking
const scrolledSections = useRef(new Set<string>());
useEffect(() => {
const ids = ['how-it-works', 'demo'];
const observers: IntersectionObserver[] = [];
ids.forEach((id) => {
const el = document.getElementById(id);
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !scrolledSections.current.has(id)) {
scrolledSections.current.add(id);
trackEvent('Scroll Depth', { section: id });
}
},
{ threshold: 0.1 }
);
observer.observe(el);
observers.push(observer);
});
return () => observers.forEach((o) => o.disconnect());
}, []);
// 30s time-on-page event
useEffect(() => {
const timer = setTimeout(() => trackEvent('Time on Page', { seconds: '30' }), 30000);
return () => clearTimeout(timer);
}, []);
return (
<div className="flex-1 overflow-y-auto bg-warm-50 dark:bg-navy-950 relative">
<div className="relative" style={{ zIndex: 1 }}>
@ -54,13 +84,17 @@ export default function HomePage({
</p>
<div className="flex items-center gap-4 mb-10">
<button
onClick={onOpenDashboard}
onClick={() => {
trackEvent('CTA Click', { location: 'hero', label: 'explore_map' });
onOpenDashboard();
}}
className="px-7 py-3.5 bg-coral-500 text-white rounded-lg font-semibold hover:bg-coral-600 transition-colors text-base shadow-lg shadow-coral-500/25"
>
Explore the map
</button>
<button
onClick={() => {
trackEvent('CTA Click', { location: 'hero', label: 'see_difference' });
const target = document.getElementById('comparison');
if (!target) return;
const scroller = target.closest('.overflow-y-auto') as HTMLElement | null;
@ -106,6 +140,7 @@ export default function HomePage({
<div className="flex-1" />
<button
onClick={() => {
trackEvent('CTA Click', { location: 'hero', label: 'see_it_in_action' });
const target = document.getElementById('demo');
if (!target) return;
const scroller = target.closest('.overflow-y-auto') as HTMLElement | null;
@ -245,7 +280,10 @@ export default function HomePage({
Don&apos;t leave it to chance.
</p>
<button
onClick={onOpenDashboard}
onClick={() => {
trackEvent('CTA Click', { location: 'bottom', label: 'explore_map' });
onOpenDashboard();
}}
className="px-8 py-4 bg-coral-500 text-white rounded-lg font-semibold hover:bg-coral-600 transition-colors text-lg shadow-lg shadow-coral-500/25"
>
Explore the map

View file

@ -1,5 +1,6 @@
import { useState, useEffect, useRef, useMemo, useDeferredValue } from 'react';
import MapComponent from '../map/Map';
import { trackEvent } from '../../lib/analytics';
import { apiUrl, assertOk, authHeaders, logNonAbortError } from '../../lib/api';
import { formatValue } from '../../lib/format';
import { zoomToResolution } from '../../lib/map-utils';
@ -147,6 +148,7 @@ export default function ScrollStory({ features, theme }: ScrollStoryProps) {
const [hexData, setHexData] = useState<HexagonData[]>([]);
const [loading, setLoading] = useState(true);
const stepRefs = useRef<(HTMLDivElement | null)[]>([]);
const trackedSteps = useRef(new Set<number>());
const abortRef = useRef<AbortController>();
const fetchTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
const sectionRef = useRef<HTMLElement>(null);
@ -182,7 +184,13 @@ export default function ScrollStory({ features, theme }: ScrollStoryProps) {
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) setStage(i);
if (entry.isIntersecting) {
setStage(i);
if (!trackedSteps.current.has(i)) {
trackedSteps.current.add(i);
trackEvent('Scroll Story Step', { step: String(i) });
}
}
},
{ rootMargin: '-35% 0px -35% 0px', threshold: 0 }
);

View file

@ -27,6 +27,7 @@ import {
type TravelTimeInitial,
} from '../../hooks/useTravelTime';
import { apiUrl, authHeaders, buildFilterString, logNonAbortError } from '../../lib/api';
import { trackEvent } from '../../lib/analytics';
import { INITIAL_VIEW_STATE } from '../../lib/consts';
import { useLicense } from '../../hooks/useLicense';
import UpgradeModal from '../ui/UpgradeModal';
@ -261,6 +262,7 @@ export default function MapPage({
link.download = 'perfect-postcode-export.xlsx';
link.click();
URL.revokeObjectURL(link.href);
trackEvent('Export');
})
.catch((err) => logNonAbortError('Export failed', err))
.finally(() => setExporting(false));
@ -270,6 +272,10 @@ export default function MapPage({
onExportStateChange?.({ onExport: handleExport, exporting });
}, [handleExport, exporting, onExportStateChange]);
useEffect(() => {
if (mapData.licenseRequired) trackEvent('Upgrade Modal Shown');
}, [mapData.licenseRequired]);
const mobileLegendMeta = useMemo(
() => (viewFeature ? features.find((f) => f.name === viewFeature) || null : null),
[viewFeature, features]

View file

@ -1,5 +1,6 @@
import { useState, useCallback } from 'react';
import { useCollapsibleGroups } from '../../hooks/useCollapsibleGroups';
import { trackEvent } from '../../lib/analytics';
import type { POICategoryGroup } from '../../types';
import InfoPopup from '../ui/InfoPopup';
import { SearchInput } from '../ui/SearchInput';
@ -31,19 +32,23 @@ export default function POIPane({
const toggleCategory = (category: string) => {
const newSet = new Set(selectedCategories);
if (newSet.has(category)) {
const wasSelected = newSet.has(category);
if (wasSelected) {
newSet.delete(category);
} else {
newSet.add(category);
}
trackEvent('POI Toggle', { category, selected: String(!wasSelected) });
onCategoriesChange(newSet);
};
const selectAll = () => {
trackEvent('POI Select All');
onCategoriesChange(new Set(allCategories));
};
const selectNone = () => {
trackEvent('POI Select None');
onCategoriesChange(new Set());
};

View file

@ -3,6 +3,7 @@ import { CheckIcon } from '../ui/icons/CheckIcon';
import { SpinnerIcon } from '../ui/icons/SpinnerIcon';
import type { AuthUser } from '../../hooks/useAuth';
import { useLicense } from '../../hooks/useLicense';
import { trackEvent } from '../../lib/analytics';
import { apiUrl } from '../../lib/api';
const FEATURES = [
@ -58,6 +59,10 @@ export default function PricingPage({
if (scrollRef.current) setScrolledLeft(scrollRef.current.scrollLeft > 0);
}, []);
useEffect(() => {
trackEvent('Pricing View');
}, []);
useEffect(() => {
fetch(apiUrl('pricing'))
.then((res) => {

View file

@ -1,6 +1,7 @@
import { useState, useCallback } from 'react';
import { useState, useCallback, useEffect } from 'react';
import { CloseIcon } from './icons/CloseIcon';
import { GoogleIcon } from './icons/GoogleIcon';
import { trackEvent } from '../../lib/analytics';
type View = 'login' | 'register' | 'forgot';
@ -30,6 +31,10 @@ export default function AuthModal({
const [password, setPassword] = useState('');
const [resetSent, setResetSent] = useState(false);
useEffect(() => {
trackEvent('Auth Modal Open', { tab: initialTab });
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const switchView = useCallback(
(newView: View) => {
setView(newView);