seems fine

This commit is contained in:
Andras Schmelczer 2026-05-05 22:29:28 +01:00
parent 48983e3b4b
commit 7a1696541f
37 changed files with 4999 additions and 1242 deletions

View file

@ -1,14 +1,16 @@
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import MapPage, { type ExportState } from './components/map/MapPage';
import PricingPage from './components/pricing/PricingPage';
import HomePage from './components/home/HomePage';
import LearnPage from './components/learn/LearnPage';
import AccountPage, { SavedPage, InvitesPage } from './components/account/AccountPage';
import InvitePage from './components/invite/InvitePage';
import { lazy, Suspense, useState, useEffect, useCallback, useMemo, useRef } from 'react';
import type { ExportState } from './components/map/MapPage';
import {
getSeoContentPage,
getSeoLandingPage,
isSeoContentKey,
isSeoLandingKey,
SEO_CONTENT_PATHS,
SEO_LANDING_PATHS,
type SeoContentKey,
type SeoLandingKey,
} from './lib/seoRoutes';
import Header, { type Page } from './components/ui/Header';
import AuthModal from './components/ui/AuthModal';
import SaveSearchModal from './components/ui/SaveSearchModal';
import LicenseSuccessModal from './components/ui/LicenseSuccessModal';
import type { FeatureMeta, FeatureGroup, POICategoriesResponse, POICategoryGroup } from './types';
import { fetchWithRetry, apiUrl } from './lib/api';
import { trackEvent } from './lib/analytics';
@ -28,6 +30,28 @@ declare global {
}
}
const HomePage = lazy(() => import('./components/home/HomePage'));
const PricingPage = lazy(() => import('./components/pricing/PricingPage'));
const LearnPage = lazy(() => import('./components/learn/LearnPage'));
const SeoLandingPage = lazy(() => import('./components/landing/SeoLandingPage'));
const SeoContentPage = lazy(() => import('./components/landing/SeoContentPage'));
const AccountPage = lazy(() => import('./components/account/AccountPage'));
const SavedPage = lazy(() =>
import('./components/account/AccountPage').then((module) => ({ default: module.SavedPage }))
);
const InvitesPage = lazy(() =>
import('./components/account/AccountPage').then((module) => ({ default: module.InvitesPage }))
);
const InvitePage = lazy(() => import('./components/invite/InvitePage'));
const MapPage = lazy(() => import('./components/map/MapPage'));
const AuthModal = lazy(() => import('./components/ui/AuthModal'));
const SaveSearchModal = lazy(() => import('./components/ui/SaveSearchModal'));
const LicenseSuccessModal = lazy(() => import('./components/ui/LicenseSuccessModal'));
function PageFallback() {
return <div className="flex-1 bg-warm-50 dark:bg-navy-950" />;
}
function pageToPath(page: Page, inviteCode?: string): string {
switch (page) {
case 'dashboard':
@ -36,6 +60,19 @@ function pageToPath(page: Page, inviteCode?: string): string {
return '/learn';
case 'pricing':
return '/pricing';
case 'property-price-map':
case 'postcode-property-search':
case 'commute-property-search':
case 'school-property-search':
case 'postcode-checker':
return SEO_LANDING_PATHS[page];
case 'birmingham-property-search':
case 'manchester-property-search':
case 'bristol-property-search':
case 'data-sources':
case 'methodology':
case 'privacy-security':
return SEO_CONTENT_PATHS[page];
case 'saved':
return '/saved';
case 'invites':
@ -55,6 +92,10 @@ function pathToPage(pathname: string): { page: Page; inviteCode?: string } | nul
if (pathname === '/invites') return { page: 'invites' };
if (pathname === '/learn') return { page: 'learn' };
if (pathname === '/pricing') return { page: 'pricing' };
const seoLandingPage = getSeoLandingPage(pathname);
if (seoLandingPage) return { page: seoLandingPage };
const seoContentPage = getSeoContentPage(pathname);
if (seoContentPage) return { page: seoContentPage };
if (pathname === '/account') return { page: 'account' };
if (pathname === '/support') return { page: 'learn' };
if (pathname.startsWith('/invite/')) {
@ -65,6 +106,14 @@ function pathToPage(pathname: string): { page: Page; inviteCode?: string } | nul
return null;
}
function isSeoLandingPage(page: Page): page is SeoLandingKey {
return isSeoLandingKey(page);
}
function isSeoContentPage(page: Page): page is SeoContentKey {
return isSeoContentKey(page);
}
export default function App() {
const urlState = useMemo(() => parseUrlState(), []);
const [mapUrlState, setMapUrlState] = useState(urlState);
@ -271,37 +320,41 @@ export default function App() {
if ((isScreenshotMode || isOgMode) && inviteCode) {
return (
<InvitePage
code={inviteCode}
user={null}
theme={theme}
screenshotMode
onLoginClick={() => {}}
onRegisterClick={() => {}}
onLicenseGranted={() => {}}
/>
<Suspense fallback={<PageFallback />}>
<InvitePage
code={inviteCode}
user={null}
theme={theme}
screenshotMode
onLoginClick={() => {}}
onRegisterClick={() => {}}
onLicenseGranted={() => {}}
/>
</Suspense>
);
}
if (isScreenshotMode) {
return (
<MapPage
features={features}
poiCategoryGroups={poiCategoryGroups}
initialFilters={urlState.filters || {}}
initialViewState={initialViewState}
initialPOICategories={urlState.poiCategories || new Set()}
initialTab={urlState.tab || 'area'}
initialLoading={initialLoading}
theme={theme}
pendingInfoFeature={null}
onClearPendingInfoFeature={() => {}}
onNavigateTo={() => {}}
screenshotMode
ogMode={isOgMode}
initialTravelTime={urlState.travelTime}
shareCode={urlState.share}
/>
<Suspense fallback={<PageFallback />}>
<MapPage
features={features}
poiCategoryGroups={poiCategoryGroups}
initialFilters={urlState.filters || {}}
initialViewState={initialViewState}
initialPOICategories={urlState.poiCategories || new Set()}
initialTab={urlState.tab || 'area'}
initialLoading={initialLoading}
theme={theme}
pendingInfoFeature={null}
onClearPendingInfoFeature={() => {}}
onNavigateTo={() => {}}
screenshotMode
ogMode={isOgMode}
initialTravelTime={urlState.travelTime}
shareCode={urlState.share}
/>
</Suspense>
);
}
@ -328,137 +381,145 @@ export default function App() {
onLogout={logout}
isMobile={isMobile}
/>
{activePage === 'home' ? (
<HomePage
onOpenDashboard={() => navigateTo('dashboard')}
onOpenPricing={() => navigateTo('pricing')}
theme={theme}
hidePricing={user?.subscription === 'licensed' || user?.isAdmin}
/>
) : activePage === 'pricing' && !(user?.subscription === 'licensed' || user?.isAdmin) ? (
<PricingPage
onOpenDashboard={() => navigateTo('dashboard')}
user={user}
onLoginClick={() => {
setAuthModalTab('login');
setShowAuthModal(true);
}}
onRegisterClick={() => {
setAuthModalTab('register');
setShowAuthModal(true);
}}
/>
) : activePage === 'learn' ? (
<LearnPage />
) : activePage === 'saved' && user ? (
<SavedPage
searches={savedSearches.searches}
searchesLoading={savedSearches.loading}
onDeleteSearch={savedSearches.deleteSearch}
onUpdateSearchNotes={savedSearches.updateSearchNotes}
onUpdateSearchName={savedSearches.updateSearchName}
onOpenSearch={(params) => {
window.location.href = `/dashboard?${params}`;
}}
savedProperties={savedProperties.properties}
propertiesLoading={savedProperties.loading}
onDeleteProperty={savedProperties.deleteProperty}
onUpdatePropertyNotes={savedProperties.updatePropertyNotes}
onOpenProperty={(postcode) => {
window.location.href = `/dashboard?pc=${encodeURIComponent(postcode)}`;
}}
/>
) : activePage === 'invites' && user ? (
<InvitesPage user={user} />
) : activePage === 'account' && user ? (
<AccountPage user={user} onRefreshAuth={refreshAuth} />
) : activePage === 'invite' && inviteCode ? (
<InvitePage
code={inviteCode}
user={user}
theme={theme}
onLoginClick={() => {
setAuthModalTab('login');
setShowAuthModal(true);
}}
onRegisterClick={() => {
setAuthModalTab('register');
setShowAuthModal(true);
}}
onLicenseGranted={() => {
setShowLicenseSuccess(true);
refreshAuth();
}}
/>
) : (
<MapPage
features={features}
poiCategoryGroups={poiCategoryGroups}
initialFilters={mapUrlState.filters || {}}
initialViewState={initialViewState}
initialPOICategories={mapUrlState.poiCategories || new Set()}
initialTab={mapUrlState.tab || 'area'}
initialLoading={initialLoading}
theme={theme}
pendingInfoFeature={pendingInfoFeature}
onClearPendingInfoFeature={() => setPendingInfoFeature(null)}
onNavigateTo={navigateTo}
onExportStateChange={setExportState}
isMobile={isMobile}
initialTravelTime={mapUrlState.travelTime}
initialPostcode={mapUrlState.postcode}
shareCode={mapUrlState.share}
user={user}
onLoginClick={() => {
setAuthModalTab('login');
setShowAuthModal(true);
}}
onRegisterClick={() => {
setAuthModalTab('register');
setShowAuthModal(true);
}}
onSaveProperty={user ? savedProperties.saveProperty : undefined}
onUnsaveProperty={user ? savedProperties.deleteProperty : undefined}
isPropertySaved={user ? savedProperties.isPropertySaved : undefined}
getSavedPropertyId={user ? savedProperties.getSavedPropertyId : undefined}
deferTutorial={showLicenseSuccess}
onSaveSearch={user ? savedSearches.saveSearch : undefined}
savingSearch={savedSearches.saving}
/>
)}
{showAuthModal && (
<AuthModal
onClose={() => setShowAuthModal(false)}
onLogin={login}
onRegister={register}
onOAuthLogin={loginWithOAuth}
onForgotPassword={requestPasswordReset}
loading={authLoading}
error={authError}
onClearError={clearError}
initialTab={authModalTab}
/>
)}
{showSaveModal && (
<SaveSearchModal
onClose={() => setShowSaveModal(false)}
onSave={savedSearches.saveSearch}
onViewSearches={() => {
setShowSaveModal(false);
navigateTo('saved');
}}
saving={savedSearches.saving}
error={savedSearches.error}
/>
)}
{showLicenseSuccess && (
<LicenseSuccessModal
onClose={() => {
setShowLicenseSuccess(false);
navigateTo('dashboard');
}}
/>
)}
<Suspense fallback={<PageFallback />}>
{activePage === 'home' ? (
<HomePage
onOpenDashboard={() => navigateTo('dashboard')}
onOpenPricing={() => navigateTo('pricing')}
theme={theme}
hidePricing={user?.subscription === 'licensed' || user?.isAdmin}
/>
) : activePage === 'pricing' && !(user?.subscription === 'licensed' || user?.isAdmin) ? (
<PricingPage
onOpenDashboard={() => navigateTo('dashboard')}
user={user}
onLoginClick={() => {
setAuthModalTab('login');
setShowAuthModal(true);
}}
onRegisterClick={() => {
setAuthModalTab('register');
setShowAuthModal(true);
}}
/>
) : activePage === 'learn' ? (
<LearnPage />
) : isSeoLandingPage(activePage) ? (
<SeoLandingPage pageKey={activePage} onOpenDashboard={() => navigateTo('dashboard')} />
) : isSeoContentPage(activePage) ? (
<SeoContentPage pageKey={activePage} onOpenDashboard={() => navigateTo('dashboard')} />
) : activePage === 'saved' && user ? (
<SavedPage
searches={savedSearches.searches}
searchesLoading={savedSearches.loading}
onDeleteSearch={savedSearches.deleteSearch}
onUpdateSearchNotes={savedSearches.updateSearchNotes}
onUpdateSearchName={savedSearches.updateSearchName}
onOpenSearch={(params) => {
window.location.href = `/dashboard?${params}`;
}}
savedProperties={savedProperties.properties}
propertiesLoading={savedProperties.loading}
onDeleteProperty={savedProperties.deleteProperty}
onUpdatePropertyNotes={savedProperties.updatePropertyNotes}
onOpenProperty={(postcode) => {
window.location.href = `/dashboard?pc=${encodeURIComponent(postcode)}`;
}}
/>
) : activePage === 'invites' && user ? (
<InvitesPage user={user} />
) : activePage === 'account' && user ? (
<AccountPage user={user} onRefreshAuth={refreshAuth} />
) : activePage === 'invite' && inviteCode ? (
<InvitePage
code={inviteCode}
user={user}
theme={theme}
onLoginClick={() => {
setAuthModalTab('login');
setShowAuthModal(true);
}}
onRegisterClick={() => {
setAuthModalTab('register');
setShowAuthModal(true);
}}
onLicenseGranted={() => {
setShowLicenseSuccess(true);
refreshAuth();
}}
/>
) : (
<MapPage
features={features}
poiCategoryGroups={poiCategoryGroups}
initialFilters={mapUrlState.filters || {}}
initialViewState={initialViewState}
initialPOICategories={mapUrlState.poiCategories || new Set()}
initialTab={mapUrlState.tab || 'area'}
initialLoading={initialLoading}
theme={theme}
pendingInfoFeature={pendingInfoFeature}
onClearPendingInfoFeature={() => setPendingInfoFeature(null)}
onNavigateTo={navigateTo}
onExportStateChange={setExportState}
isMobile={isMobile}
initialTravelTime={mapUrlState.travelTime}
initialPostcode={mapUrlState.postcode}
shareCode={mapUrlState.share}
user={user}
onLoginClick={() => {
setAuthModalTab('login');
setShowAuthModal(true);
}}
onRegisterClick={() => {
setAuthModalTab('register');
setShowAuthModal(true);
}}
onSaveProperty={user ? savedProperties.saveProperty : undefined}
onUnsaveProperty={user ? savedProperties.deleteProperty : undefined}
isPropertySaved={user ? savedProperties.isPropertySaved : undefined}
getSavedPropertyId={user ? savedProperties.getSavedPropertyId : undefined}
deferTutorial={showLicenseSuccess}
onSaveSearch={user ? savedSearches.saveSearch : undefined}
savingSearch={savedSearches.saving}
/>
)}
</Suspense>
<Suspense fallback={null}>
{showAuthModal && (
<AuthModal
onClose={() => setShowAuthModal(false)}
onLogin={login}
onRegister={register}
onOAuthLogin={loginWithOAuth}
onForgotPassword={requestPasswordReset}
loading={authLoading}
error={authError}
onClearError={clearError}
initialTab={authModalTab}
/>
)}
{showSaveModal && (
<SaveSearchModal
onClose={() => setShowSaveModal(false)}
onSave={savedSearches.saveSearch}
onViewSearches={() => {
setShowSaveModal(false);
navigateTo('saved');
}}
saving={savedSearches.saving}
error={savedSearches.error}
/>
)}
{showLicenseSuccess && (
<LicenseSuccessModal
onClose={() => {
setShowLicenseSuccess(false);
navigateTo('dashboard');
}}
/>
)}
</Suspense>
</div>
);
}