diff --git a/src/components/RouteDialog.tsx b/src/components/RouteDialog.tsx
index deddd55..2b88b8d 100644
--- a/src/components/RouteDialog.tsx
+++ b/src/components/RouteDialog.tsx
@@ -357,10 +357,17 @@ export function RouteDialog({
return () => document.removeEventListener('visibilitychange', onVisibility);
}, [following, acquireWakeLock]);
- // While following, keep the live dot centred.
+ // While following, keep the live dot centred — but only once you've actually
+ // moved, and with a gentle animation, so GPS jitter doesn't twitch the map.
+ const lastPanRef = useRef<{ lat: number; lng: number } | null>(null);
useEffect(() => {
if (!following || !live) return;
- mapRef.current?.panTo([live.lat, live.lng]);
+ const map = mapRef.current;
+ if (!map) return;
+ const last = lastPanRef.current;
+ if (last && calculateDistance(last.lat, last.lng, live.lat, live.lng) * 1000 < 6) return;
+ lastPanRef.current = { lat: live.lat, lng: live.lng };
+ map.panTo([live.lat, live.lng], { animate: true, duration: 0.6, easeLinearity: 0.5 });
}, [following, live]);
/** Live guidance: distance left, and which step you're on. */
@@ -511,9 +518,16 @@ export function RouteDialog({
};
map.on('click', onMapClick);
- setTimeout(() => map.invalidateSize(), 120);
+ // The dialog animates in (a brief scale), so the container is still
+ // settling at first paint. Nudge Leaflet's size once now and again after the
+ // animation ends — otherwise the map renders at the wrong size for a frame
+ // and visibly jumps (the "flash" people see when they tap Directions).
+ const t1 = setTimeout(() => map.invalidateSize(), 120);
+ const t2 = setTimeout(() => map.invalidateSize(), 340);
return () => {
+ clearTimeout(t1);
+ clearTimeout(t2);
map.off('click', onMapClick);
map.remove();
mapRef.current = null;
@@ -597,7 +611,7 @@ export function RouteDialog({
routeLayersRef.current = group;
if (result.coordinates.length > 0) {
- map.fitBounds(L.latLngBounds(result.coordinates), { padding: [48, 48], maxZoom: 16 });
+ map.fitBounds(L.latLngBounds(result.coordinates), { padding: [48, 48], maxZoom: 16, animate: false });
}
}
} catch (err) {
@@ -639,7 +653,7 @@ export function RouteDialog({
lineCap: 'round',
}).addTo(group);
routeLayersRef.current = group;
- map.fitBounds(L.latLngBounds(hit.route.coordinates), { padding: [48, 48], maxZoom: 16 });
+ map.fitBounds(L.latLngBounds(hit.route.coordinates), { padding: [48, 48], maxZoom: 16, animate: false });
}
}
}
@@ -677,10 +691,10 @@ export function RouteDialog({
`.leaflet-container { height: 100% }` override beats a `h-64` on the
map element itself, and against a height-less parent that resolved to
0 — a fully invisible map with no route line. */}
-
+
diff --git a/src/pages/CrewsPage.tsx b/src/pages/CrewsPage.tsx
index 37280f1..d7f0727 100644
--- a/src/pages/CrewsPage.tsx
+++ b/src/pages/CrewsPage.tsx
@@ -4,20 +4,19 @@ import { Link } from 'react-router-dom';
import { LazyMount } from '@/components/LazyMount';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
-import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
+import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
import { Skeleton } from '@/components/ui/skeleton';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { CrewRepBadge, CrewActivityPulse } from '@/components/CrewBadge';
import { EmptyState as SharedEmptyState } from '@/components/EmptyState';
import { useCrews } from '@/hooks/useCrews';
-import { useLocalStorage } from '@/hooks/useLocalStorage';
import { useCrewsActivity, crewCoordinate } from '@/hooks/useCrewActivity';
import { useGeolocation } from '@/hooks/useGeolocation';
import { crewAccent, crewDistanceKm, formatDistance, type LatLng } from '@/lib/crewHelpers';
import { CREW_VIBE_LABELS, CREW_FOCUS_LABELS, type SkateCrew, type CrewFocus } from '@/lib/types';
import type { CrewActivity } from '@/lib/crewHelpers';
import { cn } from '@/lib/utils';
-import { Users, Plus, Search, MapPin, X, HardHat, Camera, GraduationCap, Sparkles, Navigation, Loader2, Music, Palette, Dumbbell, Film, ChevronDown } from 'lucide-react';
+import { Users, Plus, Search, MapPin, X, HardHat, Camera, GraduationCap, Sparkles, Navigation, Loader2, Music, Palette, Dumbbell, Film, ChevronDown, Check } from 'lucide-react';
// `label` is the full name (used for the tooltip + screen readers); `short` is
// what the compact toggle shows, to keep the filter bar to a single row.
@@ -158,9 +157,11 @@ export default function CrewsPage() {
const geo = useGeolocation();
const [query, setQuery] = useState('');
const [focusFilter, setFocusFilter] = useState('all');
+ const [focusOpen, setFocusOpen] = useState(false);
const [nearMe, setNearMe] = useState(false);
// Remembered preference: compact short labels vs. the full filter names.
- const [filtersExpanded, setFiltersExpanded] = useLocalStorage('crews-filters-expanded', false);
+ const currentFocus = FOCUS_FILTERS.find((f) => f.key === focusFilter) ?? FOCUS_FILTERS[0];
+ const FocusIcon = currentFocus.icon;
const userLoc: LatLng | null = geo.coords;
@@ -277,53 +278,59 @@ export default function CrewsPage() {
)}
- {/* Filters: compact focus toggle group; "More" expands to full names */}
+ {/* Filters: ONE compact control instead of a horizontally-scrolling chip
+ row (on a phone the chips were clipped and had to be swiped, and the
+ expand toggle read as just another category). */}
-
- {/* Expand/collapse the filter names (remembered across visits).
- A divider + a labelled button keep it from reading as just another
- crew category sitting next to the chips. */}
-
-
+
+
+
+
+
+
+ Filter crews
+
+ What the crew is into.
+
+
+
+ A Cashu mint issues and redeems the ecash in your wallet. It holds the Bitcoin
+ backing your tokens — so a mint you don't run yourself is one you have to trust.
+
+
+
+ Mints can go offline, change terms, or disappear with funds. Keep only small
+ balances unless you trust the operator.
+
+
+ Health data is community-run (MintRadar) — unofficial and possibly stale.
+