diff --git a/AGENTS.md b/AGENTS.md index 599e1d2..7058917 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1148,6 +1148,14 @@ session with **no other memory of this project**. So: say this honestly (2026-09-29). **Still future:** real MLS (`ts-mls`) instead of group-key crypto, plus Cordn RPC interop. **Lesson: grep `src` for the feature before building it — this was nearly duplicated.** +- **Map site-links strip (2026-10-05):** the desktop footer is hidden on the map + (full-bleed), so `components/MapFooterStrip.tsx` surfaces the same links + (Newsletter/License/Privacy/Collaborators + "not sponsored by OpenSats") as an + overlay inside the map container: a **slim full-width bar on desktop** and a + **tiny collapsed "Links" chip on mobile** (expands to a compact scrollable row). + Dismissible via `useMapFooterHidden` (`skatespots:map-footer-hidden`, same-tab + synced), restorable in **Settings → Appearance**. The map's OpenStreetMap + attribution moved to `md:bottom-7` so the bar doesn't cover it. - **Map layout — controls float (2026-09-29):** on mobile the `MapPage` toolbar is **`absolute` over the map** (`pointer-events-none` wrapper + gradient + `pointer-events-auto` inner) so the map fills the whole container; `md:static` diff --git a/src/components/MapFooterStrip.tsx b/src/components/MapFooterStrip.tsx new file mode 100644 index 0000000..84a7ba5 --- /dev/null +++ b/src/components/MapFooterStrip.tsx @@ -0,0 +1,86 @@ +import { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { ChevronUp, X } from 'lucide-react'; +import { useTranslation } from '@/contexts/language'; +import { useMapFooterHidden } from '@/hooks/useMapFooterHidden'; +import { cn } from '@/lib/utils'; + +/** + * The site links that live in the desktop footer — surfaced on the map + * (a full-bleed route where the footer is hidden) without stealing map height. + * + * Desktop: a slim full-width bar. Mobile: a tiny collapsed "Links" chip that + * expands to a compact row. Dismissible, and restorable from Settings → + * Appearance. + */ +export function MapFooterStrip() { + const { t } = useTranslation(); + const [hidden, setHidden] = useMapFooterHidden(); + const [open, setOpen] = useState(false); + + if (hidden) return null; + + const links = [ + { to: '/newsletter', label: t('nav.newsletter') }, + { to: '/license', label: t('chromeUi.license') }, + { to: '/privacy', label: t('chromeUi.privacy') }, + { to: '/supporters', label: t('chromeUi.collaborators') }, + ]; + + return ( +
+ {/* Desktop — slim full-width bar */} +
+
+ {links.map((l) => ( + + {l.label} + + ))} +
+
+ + {t('chromeUi.notSponsoredBy')} + OpenSats + OpenSats + + +
+
+ + {/* Mobile — a tiny chip that expands to a compact row */} +
+ {open && ( +
+ {links.map((l) => ( + + {l.label} + + ))} + +
+ )} + +
+
+ ); +} diff --git a/src/hooks/useMapFooterHidden.ts b/src/hooks/useMapFooterHidden.ts new file mode 100644 index 0000000..902bf88 --- /dev/null +++ b/src/hooks/useMapFooterHidden.ts @@ -0,0 +1,30 @@ +import { useCallback, useEffect } from 'react'; +import { useLocalStorage } from '@/hooks/useLocalStorage'; + +const KEY = 'skatespots:map-footer-hidden'; +const EVENT = 'mapfooter:hiddenchange'; + +/** + * Whether the thin links strip at the bottom of the map is hidden. Device-local + * and synced within the tab via a custom event (so the Settings toggle and the + * strip stay in step). Restorable from Settings → Appearance. + */ +export function useMapFooterHidden(): [boolean, (value: boolean) => void] { + const [hidden, setHiddenRaw] = useLocalStorage(KEY, false); + + useEffect(() => { + const handler = (e: Event) => setHiddenRaw(!!(e as CustomEvent).detail); + window.addEventListener(EVENT, handler); + return () => window.removeEventListener(EVENT, handler); + }, [setHiddenRaw]); + + const setHidden = useCallback( + (value: boolean) => { + setHiddenRaw(value); + window.dispatchEvent(new CustomEvent(EVENT, { detail: value })); + }, + [setHiddenRaw], + ); + + return [hidden, setHidden]; +} diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 7903f83..163141a 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -790,6 +790,10 @@ nav: { }, mapUi: { + mapFooter: { + links: 'Links', + hide: 'Hide map links bar', + }, // Map chrome findingSpots: 'Finding spots', loadingSpots: 'Loading spots', @@ -5109,6 +5113,8 @@ settingsUi: { assistant: 'Assistant', showSasquatch: 'Show Sasquatch assistant', showSasquatchDesc: 'Show the floating Sasquatch button on the map and other screens.', + showMapFooter: 'Show map links bar', + showMapFooterDesc: 'Show the thin links strip at the bottom of the map.', notifications: 'Notifications', notificationsSub: 'Alerts for DMs, crews and zaps', coordinator: 'Coordinator', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 2cb071b..6d80dd5 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -790,6 +790,10 @@ nav: { }, mapUi: { + mapFooter: { + links: 'Enlaces', + hide: 'Ocultar barra de enlaces del mapa', + }, // Map chrome findingSpots: 'Buscando spots', loadingSpots: 'Cargando spots', @@ -5109,6 +5113,8 @@ settingsUi: { assistant: 'Asistente', showSasquatch: 'Mostrar el asistente Sasquatch', showSasquatchDesc: 'Muestra el botón flotante del Sasquatch en el mapa y otras pantallas.', + showMapFooter: 'Mostrar barra de enlaces del mapa', + showMapFooterDesc: 'Muestra la franja fina de enlaces en la parte inferior del mapa.', notifications: 'Notificaciones', notificationsSub: 'Avisos de MD, crews y zaps', coordinator: 'Coordinador', diff --git a/src/pages/MapPage.tsx b/src/pages/MapPage.tsx index 60aa3b4..7b7e834 100644 --- a/src/pages/MapPage.tsx +++ b/src/pages/MapPage.tsx @@ -5,6 +5,7 @@ import { Link, useNavigate, useSearchParams } from 'react-router-dom'; // Lazy-load the map — Leaflet is large (~150KB) and only needed on this page const SpotMap = lazy(() => import('@/components/SpotMap').then(m => ({ default: m.SpotMap }))); import { SpotCard, SpotCardSkeleton } from '@/components/SpotCard'; +import { MapFooterStrip } from '@/components/MapFooterStrip'; import { WeatherToggle } from '@/components/WeatherToggle'; import { RouteDialog } from '@/components/RouteDialog'; import { TrustFilter } from '@/components/TrustFilter'; @@ -1833,7 +1834,7 @@ export default function MapPage() { {/* Map attribution — bottom-left, sits ABOVE the mobile bottom nav so it never covers the Shops & Spots buttons. Tiny + subtle. */}
© OpenStreetMap @@ -1843,6 +1844,10 @@ export default function MapPage() {
+ {/* Slim, dismissible site-links strip — the desktop footer is hidden + on this full-bleed route, so surface the links here instead. */} + + {/* Location permission help dialog */} {showLocationHelp && (
diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 24cc7b9..2897be2 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -3,6 +3,7 @@ import { Link, useSearchParams } from 'react-router-dom'; import { EditProfileForm } from '@/components/EditProfileForm'; import { Switch } from '@/components/ui/switch'; import { useSasquatchHidden } from '@/hooks/useSasquatchHidden'; +import { useMapFooterHidden } from '@/hooks/useMapFooterHidden'; import { SkaterDirectoryCard } from '@/components/SkaterDirectoryCard'; import { ProfilePreviewCard } from '@/components/ProfilePreviewCard'; import { RelayListManager } from '@/components/RelayListManager'; @@ -614,6 +615,7 @@ function RelaysScreen({ onBack }: { onBack: () => void }) { function AppearanceScreen({ onBack }: { onBack: () => void }) { const { t } = useTranslation(); const [sasquatchHidden, setSasquatchHidden] = useSasquatchHidden(); + const [mapFooterHidden, setMapFooterHidden] = useMapFooterHidden(); return (
@@ -636,6 +638,13 @@ function AppearanceScreen({ onBack }: { onBack: () => void }) {
setSasquatchHidden(!v)} aria-label={t('settingsUi.settingsPage.showSasquatch')} />
+
+
+

{t('settingsUi.settingsPage.showMapFooter')}

+

{t('settingsUi.settingsPage.showMapFooterDesc')}

+
+ setMapFooterHidden(!v)} aria-label={t('settingsUi.settingsPage.showMapFooter')} /> +