Add a slim, dismissible site-links strip to the map
The desktop footer is hidden on full-bleed map routes, leaving the Newsletter/License/Privacy/Collaborators links (and the "not sponsored by OpenSats" note) unreachable from the homepage. MapFooterStrip overlays them inside the map container without stealing map height: - Desktop: a slim full-width bottom bar. - Mobile: a tiny collapsed "Links" chip that expands to a compact, scrollable row. - Dismissible (useMapFooterHidden, device-local, same-tab synced) and restorable from Settings -> Appearance. - The map's OSM attribution moved up (md:bottom-7) so the bar doesn't cover it.
This commit is contained in:
parent
51ff603670
commit
b37d627138
7 changed files with 151 additions and 1 deletions
|
|
@ -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`
|
||||
|
|
|
|||
86
src/components/MapFooterStrip.tsx
Normal file
86
src/components/MapFooterStrip.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-[1000]">
|
||||
{/* Desktop — slim full-width bar */}
|
||||
<div className="pointer-events-auto hidden w-full items-center justify-between gap-4 border-t border-white/10 bg-black/85 px-4 py-1.5 text-[11px] text-gray-400 backdrop-blur md:flex">
|
||||
<div className="flex items-center gap-4">
|
||||
{links.map((l) => (
|
||||
<Link key={l.to} to={l.to} className="font-medium transition-colors hover:text-[#ccff00]">
|
||||
{l.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex items-center gap-1.5 text-gray-500">
|
||||
{t('chromeUi.notSponsoredBy')}
|
||||
<img src="https://opensats.org/favicon.ico" alt="OpenSats" className="h-3 w-3" />
|
||||
<span className="font-medium text-gray-400">OpenSats</span>
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setHidden(true)}
|
||||
aria-label={t('mapUi.mapFooter.hide')}
|
||||
className="min-h-0 min-w-0 text-gray-500 transition-colors hover:text-white"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile — a tiny chip that expands to a compact row */}
|
||||
<div className="pointer-events-auto relative mb-1.5 flex justify-center md:hidden">
|
||||
{open && (
|
||||
<div className="absolute bottom-full left-1/2 mb-1.5 flex max-w-[calc(100vw-1rem)] -translate-x-1/2 items-center gap-3 overflow-x-auto whitespace-nowrap rounded-full border border-white/10 bg-black/85 px-3 py-1.5 text-[10px] text-gray-300 backdrop-blur scrollbar-hide">
|
||||
{links.map((l) => (
|
||||
<Link key={l.to} to={l.to} className="font-medium hover:text-[#ccff00]">
|
||||
{l.label}
|
||||
</Link>
|
||||
))}
|
||||
<button
|
||||
onClick={() => setHidden(true)}
|
||||
aria-label={t('mapUi.mapFooter.hide')}
|
||||
className="min-h-0 min-w-0 text-gray-500 hover:text-white"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
aria-expanded={open}
|
||||
className="flex min-h-0 items-center gap-1 rounded-full bg-black/45 px-2.5 py-[3px] text-[10px] font-semibold text-gray-400/90 backdrop-blur transition-colors hover:text-gray-200"
|
||||
>
|
||||
<ChevronUp className={cn('h-3 w-3 transition-transform', open && 'rotate-180')} />
|
||||
{t('mapUi.mapFooter.links')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
30
src/hooks/useMapFooterHidden.ts
Normal file
30
src/hooks/useMapFooterHidden.ts
Normal file
|
|
@ -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<boolean>).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];
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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. */}
|
||||
<div
|
||||
className="absolute left-1.5 z-[999] pointer-events-none bottom-[calc(5rem+env(safe-area-inset-bottom,0px)+0.25rem)] md:bottom-1.5"
|
||||
className="absolute left-1.5 z-[999] pointer-events-none bottom-[calc(5rem+env(safe-area-inset-bottom,0px)+0.25rem)] md:bottom-7"
|
||||
>
|
||||
<div className="bg-black/55 backdrop-blur-sm px-1.5 py-0.5 rounded text-[9px] font-medium text-gray-200/90 leading-none pointer-events-auto">
|
||||
<a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener noreferrer" className="hover:text-white transition-colors">© OpenStreetMap</a>
|
||||
|
|
@ -1843,6 +1844,10 @@ export default function MapPage() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Slim, dismissible site-links strip — the desktop footer is hidden
|
||||
on this full-bleed route, so surface the links here instead. */}
|
||||
<MapFooterStrip />
|
||||
|
||||
{/* Location permission help dialog */}
|
||||
{showLocationHelp && (
|
||||
<div className="absolute inset-0 z-[2000] flex items-end justify-center pb-[calc(5rem+env(safe-area-inset-bottom,0px))] md:items-center md:pb-0 px-4">
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div>
|
||||
<BackHeader title={t('settingsUi.settingsPage.appearance')} onBack={onBack} />
|
||||
|
|
@ -636,6 +638,13 @@ function AppearanceScreen({ onBack }: { onBack: () => void }) {
|
|||
</div>
|
||||
<Switch checked={!sasquatchHidden} onCheckedChange={(v) => setSasquatchHidden(!v)} aria-label={t('settingsUi.settingsPage.showSasquatch')} />
|
||||
</div>
|
||||
<div className="mt-2 rounded-2xl border border-white/8 p-4 bg-white/2 flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-bold text-white">{t('settingsUi.settingsPage.showMapFooter')}</p>
|
||||
<p className="text-xs text-muted-foreground">{t('settingsUi.settingsPage.showMapFooterDesc')}</p>
|
||||
</div>
|
||||
<Switch checked={!mapFooterHidden} onCheckedChange={(v) => setMapFooterHidden(!v)} aria-label={t('settingsUi.settingsPage.showMapFooter')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-white/8 p-5 bg-white/2 text-center">
|
||||
|
|
|
|||
Loading…
Reference in a new issue