ok
This commit is contained in:
parent
c2070693fb
commit
909e241907
55 changed files with 594 additions and 223 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
17
frontend/src/index-html.test.ts
Normal file
17
frontend/src/index-html.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue