diff --git a/docker-compose.yml b/docker-compose.yml index 9322da0..4979d0a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -58,6 +58,7 @@ services: PORT: "8002" APP_URL: http://frontend:3001 CACHE_DIR: /cache + SCREENSHOT_CACHE_ENABLED: "false" SCREENSHOT_CONCURRENCY: "3" SCREENSHOT_RATE_WINDOW_MS: "60000" SCREENSHOT_RATE_LIMIT: "30" diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 12fc9fa..0e0a0c1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -12,16 +12,16 @@ import { } from './lib/seoRoutes'; import Header, { type Page } from './components/ui/Header'; import type { FeatureMeta, FeatureGroup, POICategoriesResponse, POICategoryGroup } from './types'; -import { fetchWithRetry, apiUrl } from './lib/api'; +import { fetchWithRetry, apiUrl, logNonAbortError } from './lib/api'; import { trackEvent } from './lib/analytics'; import { parseUrlState } from './lib/url-state'; import { INITIAL_VIEW_STATE } from './lib/consts'; import { useTheme } from './hooks/useTheme'; import { useIsMobile } from './hooks/useIsMobile'; import { useAuth } from './hooks/useAuth'; +import { useLicense } from './hooks/useLicense'; import { useTelemetry } from './hooks/useTelemetry'; import { useSavedSearches } from './hooks/useSavedSearches'; -import { useSavedProperties } from './hooks/useSavedProperties'; declare global { interface Window { @@ -39,9 +39,6 @@ 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')); @@ -52,6 +49,49 @@ function PageFallback() { return
; } +interface RouteMatch { + page: Page; + inviteCode?: string; + hash?: string; +} + +type PostAuthIntent = 'checkout'; +type LicenseSuccessStatus = 'hidden' | 'verifying' | 'success' | 'delayed'; + +const LICENSE_VERIFICATION_ATTEMPTS = 8; +const LICENSE_VERIFICATION_DELAY_MS = 1500; + +function hasFullAccess(user?: { subscription?: string; isAdmin?: boolean } | null): boolean { + return user?.subscription === 'licensed' || user?.isAdmin === true; +} + +function delay(ms: number): Promise- {t('savedPage.noSavedProperties')} -
-- {t('savedPage.noSavedPropertiesDesc')} -
-{prop.postcode}
- {price && ( -{price}
- )} - {details && ( -{details}
- )} -- {formatRelativeTime(prop.created)} -
- -{t('accountPage.needHelp')}
diff --git a/frontend/src/components/home/HomeFinalCta.tsx b/frontend/src/components/home/HomeFinalCta.tsx index 0d3158d..e114b10 100644 --- a/frontend/src/components/home/HomeFinalCta.tsx +++ b/frontend/src/components/home/HomeFinalCta.tsx @@ -5,7 +5,7 @@ const HOME_SECTION_HEADING_CLASS = 'text-2xl md:text-3xl font-bold text-navy-950 dark:text-warm-100'; const HOME_BODY_CLASS = 'text-base leading-relaxed text-warm-600 dark:text-warm-400'; const HOME_PRIMARY_BUTTON_CLASS = - 'bg-coral-500 text-white rounded-lg font-semibold hover:bg-coral-600 transition-colors text-base shadow-lg shadow-coral-500/25 text-center'; + 'border border-[#d27a11] bg-[#f09a22] text-navy-950 rounded-lg font-semibold hover:bg-[#df8614] transition-colors text-base shadow-lg shadow-[#7a3905]/25 text-center'; export default function HomeFinalCta({ onOpenDashboard, diff --git a/frontend/src/components/home/HomePage.tsx b/frontend/src/components/home/HomePage.tsx index 35a0ffe..8e929ff 100644 --- a/frontend/src/components/home/HomePage.tsx +++ b/frontend/src/components/home/HomePage.tsx @@ -1,6 +1,7 @@ import { lazy, Suspense, useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useFadeInRef } from '../../hooks/useFadeIn'; +import { useIsMobile } from '../../hooks/useIsMobile'; import HexCanvas from './HexCanvas'; import HomeFinalCta from './HomeFinalCta'; import BottomIllustration from './BottomIllustration'; @@ -15,7 +16,7 @@ const HOME_SECTION_HEADING_CLASS = 'text-2xl md:text-3xl font-bold text-navy-950 dark:text-warm-100'; const HOME_BODY_CLASS = 'text-base leading-relaxed text-warm-600 dark:text-warm-400'; const HOME_PRIMARY_BUTTON_CLASS = - 'bg-coral-500 text-white rounded-lg font-semibold hover:bg-coral-600 transition-colors text-base shadow-lg shadow-coral-500/25 text-center'; + 'border border-[#d27a11] bg-[#f09a22] text-navy-950 rounded-lg font-semibold hover:bg-[#df8614] transition-colors text-base shadow-lg shadow-[#7a3905]/25 text-center'; const PRODUCT_DEMO_VIDEO_BY_LANGUAGE: Record{t('learnPage.articlesIntro')}
++ {link.description} +
+ + ))} +
+
{t('licenseSuccess.subtitle')}
+{subtitle}
- {t('licenseSuccess.description')} -
-{description}
+ {!isVerifying && ( +