fmt
All checks were successful
Build and publish Docker image / build-and-push (push) Successful in 2m2s
CI / Check (push) Successful in 8m38s

This commit is contained in:
Andras Schmelczer 2026-06-26 20:18:38 +01:00
parent 7bc591be2b
commit f03edb86c1
2 changed files with 38 additions and 3 deletions

View file

@ -148,7 +148,15 @@ export default function MapPage({
// registered free → REGISTERED_MAX_FILTERS, licensed/admin → unlimited. Hitting the // registered free → REGISTERED_MAX_FILTERS, licensed/admin → unlimited. Hitting the
// cap opens the upgrade modal. // cap opens the upgrade modal.
const isLoggedIn = !!user; 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 45-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 effectiveFilterCap = isLoggedIn ? REGISTERED_MAX_FILTERS : DEMO_MAX_FILTERS;
const filterLimit = filtersUnlimited ? null : effectiveFilterCap; const filterLimit = filtersUnlimited ? null : effectiveFilterCap;
// On a shared link, non-paying users may view and adjust the shared filters but // 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( const handleAiFilterSubmit = useCallback(
async (query: string) => { 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 = { const context = {
filters, filters,
// Send the resolved variant so the AI sees the actual server mode in use, // Send the resolved variant so the AI sees the actual server mode in use,
@ -351,6 +367,8 @@ export default function MapPage({
fetchAiFilters, fetchAiFilters,
filters, filters,
filtersUnlimited, filtersUnlimited,
lockFilterAdds,
handleFilterLimitReached,
getMobileMapFlyToOptions, getMobileMapFlyToOptions,
handleSetEntries, handleSetEntries,
handleSetFilters, handleSetFilters,
@ -707,10 +725,13 @@ export default function MapPage({
// link adds are blocked regardless of count, so don't auto-clear there — the user // 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. // dismisses it themselves. Prevents a stale flag from resurfacing spuriously.
useEffect(() => { useEffect(() => {
if (filtersUnlimited || (!lockFilterAdds && Object.keys(filters).length < effectiveFilterCap)) { if (
filtersUnlimited ||
(!lockFilterAdds && Object.keys(filters).length + entries.length < effectiveFilterCap)
) {
setFilterLimitHit(false); setFilterLimitHit(false);
} }
}, [filtersUnlimited, lockFilterAdds, effectiveFilterCap, filters]); }, [filtersUnlimited, lockFilterAdds, effectiveFilterCap, filters, entries]);
const handleUpgradeClick = useCallback(() => { const handleUpgradeClick = useCallback(() => {
onNavigateTo('pricing'); onNavigateTo('pricing');

View file

@ -695,6 +695,20 @@ export function useFilters({
// per-add path). Without this, an unlicensed user could end up with >5 filters, // per-add path). Without this, an unlicensed user could end up with >5 filters,
// which the server then rejects (400), breaking the map. // which the server then rejects (400), breaking the map.
let next = newFilters; 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; const limit = filterLimitRef.current;
if (limit != null && Object.keys(next).length > limit) { if (limit != null && Object.keys(next).length > limit) {
next = Object.fromEntries(Object.entries(next).slice(0, limit)); next = Object.fromEntries(Object.entries(next).slice(0, limit));