This commit is contained in:
Andras Schmelczer 2026-07-03 18:01:10 +01:00
parent c2070693fb
commit 909e241907
55 changed files with 594 additions and 223 deletions

View file

@ -17,7 +17,7 @@ import { fetchWithRetry, apiUrl, logNonAbortError } from './lib/api';
import { trackEvent } from './lib/analytics';
import { parseUrlState } from './lib/url-state';
import pb from './lib/pocketbase';
import { INITIAL_VIEW_STATE } from './lib/consts';
import { DEFAULT_DEMO_FILTERS, INITIAL_VIEW_STATE } from './lib/consts';
import { useTheme } from './hooks/useTheme';
import { useIsMobile } from './hooks/useIsMobile';
import { useAuth } from './hooks/useAuth';
@ -86,7 +86,7 @@ function persistLastDashboardParams(params: string) {
try {
if (params) window.localStorage.setItem(LAST_DASHBOARD_PARAMS_KEY, params);
} catch {
// Storage unavailable (private mode/quota) — session restore is best-effort.
// Storage unavailable (private mode/quota). Session restore is best-effort.
}
}
@ -234,6 +234,16 @@ export default function App() {
return params.get('og') === '1';
}, []);
// Funnel fix: pre-seed high-intent filters on a cold (empty) map open so first-time visitors
// immediately see value and are one filter from the demo cap. Deep links (OG screenshots, the
// SEO landing-page CTAs) already carry filters, so they're left as-is. See DEFAULT_DEMO_FILTERS.
const initialMapFilters = useMemo(() => {
if (isScreenshotMode || isOgMode) return mapUrlState.filters;
return Object.keys(mapUrlState.filters).length === 0
? { ...DEFAULT_DEMO_FILTERS }
: mapUrlState.filters;
}, [mapUrlState.filters, isScreenshotMode, isOgMode]);
const [features, setFeatures] = useState<FeatureMeta[]>([]);
const [poiCategoryGroups, setPOICategoryGroups] = useState<POICategoryGroup[]>([]);
const [initialLoading, setInitialLoading] = useState(true);
@ -253,7 +263,7 @@ export default function App() {
// Restore from history state (e.g. popstate)
if (window.history.state?.page) return window.history.state.page;
// Unknown path track as 404
// Unknown path: track as 404
if (window.location.pathname !== '/') {
trackEvent('404', { path: window.location.pathname });
}
@ -267,6 +277,7 @@ export default function App() {
user,
loading: authLoading,
error: authError,
errorAction: authErrorAction,
login,
register,
loginWithOAuth,
@ -346,7 +357,7 @@ export default function App() {
async function refreshOnStartup() {
if (!returnedFromCheckout) {
// Refresh auth on startup to pick up server-side subscription changes,
// but only when a token exists — logged-out visitors would just 401.
// but only when a token exists. Logged-out visitors would just 401.
if (pb.authStore.token) {
refreshAuthRef.current().catch(() => {});
}
@ -517,7 +528,7 @@ export default function App() {
activePageRef.current = activePage;
}, [activePage]);
// Refs for the initial history.replaceState seed below — the popstate effect runs
// Refs for the initial history.replaceState seed below. The popstate effect runs
// mount-only, but it needs to read the *initial* page/hash/inviteCode values once.
const initialPageRef = useRef(activePage);
const initialRouteHashRef = useRef(routeHash);
@ -736,7 +747,7 @@ export default function App() {
key={dashboardRouteKey}
features={features}
poiCategoryGroups={poiCategoryGroups}
initialFilters={mapUrlState.filters}
initialFilters={initialMapFilters}
initialViewState={initialViewState}
initialPOICategories={mapUrlState.poiCategories}
initialOverlays={mapUrlState.overlays}
@ -793,6 +804,7 @@ export default function App() {
onForgotPassword={requestPasswordReset}
loading={authLoading}
error={authError}
errorAction={authErrorAction}
onClearError={clearError}
initialTab={authModalTab}
valuePropKey={authModalValuePropKey ?? undefined}

View file

@ -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 =
'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';
'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 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-300/80';
export default function HomeFinalCta({
onOpenDashboard,
@ -31,6 +31,9 @@ export default function HomeFinalCta({
>
{t('home.exploreTheMap')}
</button>
<p className="mt-4 text-sm font-medium text-warm-600 dark:text-warm-400">
{t('home.guaranteeNote')}
</p>
</div>
);
}

View file

@ -0,0 +1,17 @@
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { describe, it, expect } from 'vitest';
describe('index.html viewport', () => {
// vitest runs with cwd at the frontend package root.
const html = readFileSync(resolve(process.cwd(), 'src/index.html'), 'utf-8');
it('allows pinch-zoom (WCAG 1.4.4)', () => {
const m = html.match(/<meta name="viewport" content="([^"]*)"/);
expect(m).not.toBeNull();
const content = m![1];
expect(content).not.toMatch(/maximum-scale/);
expect(content).not.toMatch(/user-scalable\s*=\s*no/);
expect(content).toContain('width=device-width');
expect(content).toContain('initial-scale=1');
});
});

View file

@ -456,7 +456,7 @@ h3 {
}
}
/* MapLibre scale control — dark mode */
/* MapLibre scale control (dark mode) */
.dark .maplibregl-ctrl-scale {
border-color: #d6d3d1;
color: #d6d3d1;
@ -467,7 +467,7 @@ h3 {
bottom: calc(var(--map-mobile-bottom-inset, 0px) + 0.25rem) !important;
}
/* Highlight flash for newly added filter cards animated green stroke. */
/* Highlight flash for newly added filter cards: animated green stroke. */
@property --filter-highlight-angle {
syntax: '<angle>';
initial-value: 0deg;