lgtm
Some checks failed
CI / Check (push) Failing after 2m47s
Build and publish Docker image / build-and-push (push) Successful in 5m37s

This commit is contained in:
Andras Schmelczer 2026-06-26 16:48:20 +01:00
parent 5e73287eaf
commit e2b85fe819
73 changed files with 1180 additions and 2028 deletions

View file

@ -12,7 +12,7 @@ import {
} from './lib/seoRoutes';
import Header, { type Page } from './components/ui/Header';
import type { FeatureMeta, FeatureGroup, POICategoriesResponse, POICategoryGroup } from './types';
import { fetchWithRetry, apiUrl, logNonAbortError, readDemoChoice, setDemoCenter } from './lib/api';
import { fetchWithRetry, apiUrl, logNonAbortError } from './lib/api';
import { trackEvent } from './lib/analytics';
import { parseUrlState } from './lib/url-state';
import pb from './lib/pocketbase';
@ -209,34 +209,6 @@ export default function App() {
}, []);
const initialRoute = useMemo(() => pathToPage(window.location.pathname), []);
const [mapUrlState, setMapUrlState] = useState(urlState);
// Demo location: a fresh visitor (no explicit URL view) is offered the "check
// your area" prompt (in MapPage). A choice made earlier this session is reused —
// GPS coords re-centre the map and are sent to the server (via lib/api), while
// 'declined' keeps Canary Wharf. Resolve it once, before children fetch, so the
// very first data request already carries the centre.
// Read license synchronously from the persisted session so we never apply the
// demo location for a returning licensed user (they bypass the free zone, and we
// don't want their location in request URLs).
const licensedAtMount = useMemo(
() =>
pb.authStore.isValid &&
(pb.authStore.record?.subscription === 'licensed' || pb.authStore.record?.is_admin === true),
[]
);
const initialDemoChoice = useMemo(() => {
const choice = readDemoChoice();
if (choice && choice !== 'declined' && !licensedAtMount) setDemoCenter(choice);
return choice;
}, [licensedAtMount]);
const [demoView] = useState<typeof INITIAL_VIEW_STATE | null>(() =>
initialDemoChoice && initialDemoChoice !== 'declined' && !licensedAtMount
? {
...INITIAL_VIEW_STATE,
latitude: initialDemoChoice.lat,
longitude: initialDemoChoice.lon,
}
: null
);
const [dashboardRouteKey, setDashboardRouteKey] = useState(() =>
window.location.pathname === '/dashboard' ? window.location.search : ''
);
@ -248,8 +220,8 @@ export default function App() {
);
const activePageRef = useRef<Page>('home');
const initialViewState = useMemo(
() => (mapUrlState.hasExplicitView ? mapUrlState.viewState : (demoView ?? INITIAL_VIEW_STATE)),
[mapUrlState.hasExplicitView, mapUrlState.viewState, demoView]
() => (mapUrlState.hasExplicitView ? mapUrlState.viewState : INITIAL_VIEW_STATE),
[mapUrlState.hasExplicitView, mapUrlState.viewState]
);
const isScreenshotMode = useMemo(() => {
@ -302,16 +274,6 @@ export default function App() {
refreshAuth,
clearError,
} = useAuth();
// Only offer the "check your area" demo prompt to non-licensed visitors on a
// fresh visit. `user` is hydrated synchronously from the persisted PocketBase
// session, so a returning premium user is recognised at mount (no flash).
const offerDemoLocation =
!urlState.hasExplicitView && initialDemoChoice == null && !hasFullAccess(user);
// Stop transmitting the demo location once the user is licensed (in-session
// upgrade, cross-tab sign-in, or a stale session that refreshed to licensed).
useEffect(() => {
if (hasFullAccess(user)) setDemoCenter(null);
}, [user]);
const { startCheckout: startPostAuthCheckout } = useLicense();
const [showAuthModal, setShowAuthModal] = useState(false);
const [authModalTab, setAuthModalTab] = useState<'login' | 'register'>('login');
@ -351,8 +313,8 @@ export default function App() {
if (!completed) {
setPostAuthIntent(null);
postAuthCheckoutReturnPathRef.current = null;
// Only protected pages bounce home; the dashboard stays open in demo
// mode (server-enforced free zone) when the modal is dismissed.
// Only protected pages bounce home; the dashboard stays open when the
// modal is dismissed.
if (isProtectedPage(activePageRef.current)) {
window.history.replaceState({ page: 'home', hash: '' }, '', '/');
setRouteHash('');
@ -611,9 +573,8 @@ export default function App() {
const isProtectedPageActive = isProtectedPage(activePage);
// Only protected pages (account/saved) prompt for login on entry. The
// dashboard opens straight into demo mode (server-enforced free zone) so
// visitors can try it without logging in; the upgrade modal still appears
// when they pan outside the free zone.
// dashboard opens straight away so visitors can try it without logging in;
// the upgrade modal appears when they hit the free filter cap.
useEffect(() => {
if (authLoading) return;
if (isProtectedPageActive && !user) {
@ -768,7 +729,6 @@ export default function App() {
poiCategoryGroups={poiCategoryGroups}
initialFilters={mapUrlState.filters}
initialViewState={initialViewState}
offerDemoLocation={offerDemoLocation}
initialPOICategories={mapUrlState.poiCategories}
initialOverlays={mapUrlState.overlays}
initialCrimeTypes={mapUrlState.crimeTypes}