From f03edb86c1d3beda767f54ecdbd0544412b302bd Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Fri, 26 Jun 2026 20:18:38 +0100 Subject: [PATCH] fmt --- frontend/src/components/map/MapPage.tsx | 27 ++++++++++++++++++++++--- frontend/src/hooks/useFilters.ts | 14 +++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/map/MapPage.tsx b/frontend/src/components/map/MapPage.tsx index ee9fd93..a8bb6e2 100644 --- a/frontend/src/components/map/MapPage.tsx +++ b/frontend/src/components/map/MapPage.tsx @@ -148,7 +148,15 @@ export default function MapPage({ // registered free → REGISTERED_MAX_FILTERS, licensed/admin → unlimited. Hitting the // cap opens the upgrade modal. const isLoggedIn = !!user; - const filtersUnlimited = user?.subscription === 'licensed' || user?.isAdmin === true; + // Screenshot/OG renders are non-interactive previews of a (possibly shared) search. + // They go through the server's internal-request exemption, so they must render the + // full filter set rather than the capped demo view — otherwise a 4–5-filter shared + // search is silently trimmed to the anonymous cap (3) in the generated preview image. + const filtersUnlimited = + screenshotMode === true || + ogMode === true || + user?.subscription === 'licensed' || + user?.isAdmin === true; const effectiveFilterCap = isLoggedIn ? REGISTERED_MAX_FILTERS : DEMO_MAX_FILTERS; const filterLimit = filtersUnlimited ? null : effectiveFilterCap; // On a shared link, non-paying users may view and adjust the shared filters but @@ -263,6 +271,14 @@ export default function MapPage({ const handleAiFilterSubmit = useCallback( async (query: string) => { + // On a shared link, non-paying users may adjust the shared filters but not build + // a new set. The AI box is a "build me a search" tool (and it also drives + // travel-time entries, which bypass the per-add lock), so block it outright here + // — mirroring handleAddTravelTimeEntry — and surface the upsell instead. + if (lockFilterAdds) { + handleFilterLimitReached(); + return; + } const context = { filters, // Send the resolved variant so the AI sees the actual server mode in use, @@ -351,6 +367,8 @@ export default function MapPage({ fetchAiFilters, filters, filtersUnlimited, + lockFilterAdds, + handleFilterLimitReached, getMobileMapFlyToOptions, handleSetEntries, handleSetFilters, @@ -707,10 +725,13 @@ export default function MapPage({ // link adds are blocked regardless of count, so don't auto-clear there — the user // dismisses it themselves. Prevents a stale flag from resurfacing spuriously. useEffect(() => { - if (filtersUnlimited || (!lockFilterAdds && Object.keys(filters).length < effectiveFilterCap)) { + if ( + filtersUnlimited || + (!lockFilterAdds && Object.keys(filters).length + entries.length < effectiveFilterCap) + ) { setFilterLimitHit(false); } - }, [filtersUnlimited, lockFilterAdds, effectiveFilterCap, filters]); + }, [filtersUnlimited, lockFilterAdds, effectiveFilterCap, filters, entries]); const handleUpgradeClick = useCallback(() => { onNavigateTo('pricing'); diff --git a/frontend/src/hooks/useFilters.ts b/frontend/src/hooks/useFilters.ts index 118cd0f..27343b4 100644 --- a/frontend/src/hooks/useFilters.ts +++ b/frontend/src/hooks/useFilters.ts @@ -695,6 +695,20 @@ export function useFilters({ // per-add path). Without this, an unlicensed user could end up with >5 filters, // which the server then rejects (400), breaking the map. let next = newFilters; + // On a shared link (lockAdds), a non-paying user may adjust the shared filters + // but not introduce new keys — enforce that on bulk sets too, not just in the + // per-add path, so the AI/bulk path can't be used to build a fresh set on a + // shared link. Keep only keys already present; signal if any were dropped. + if (lockAddsRef.current) { + const current = filtersRef.current; + const restricted = Object.fromEntries( + Object.entries(next).filter(([name]) => name in current) + ); + if (Object.keys(restricted).length !== Object.keys(next).length) { + onFilterLimitReachedRef.current?.(); + } + next = restricted; + } const limit = filterLimitRef.current; if (limit != null && Object.keys(next).length > limit) { next = Object.fromEntries(Object.entries(next).slice(0, limit));