UX audit fixes: iOS directions, Me tab profile link, onboarding banner, empty states, stats deltas, newsletter pill
- Fix iOS directions: Apple Maps first on Apple platforms, removed broken geo: URI, added Waze option, fixed popover z-index clipping behind Sheet component - Fix 'Me' bottom nav: now deep-links to /settings?section=profile for logged-in users so tapping 'Me' shows your profile, not a generic settings menu - Add SettingsPage URL param support: ?section=X opens sub-screen directly - Add onboarding welcome banner: dismissible strip at top of map for logged-out users with 'Get Started' CTA pointing to /onboarding, stored in localStorage - Fix Lab empty state: FAQ link + login prompt for logged-out users - Fix Events empty state: FAQ link added to all empty state instances - Fix GearSwap empty state: actionable 'Clear Filters' button + FAQ link - Community stats: add 'this month' delta badges (+N this month) on all stat cards - Newsletter pill: hide on map page to eliminate bottom sheet overlap conflict Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
3e6fc305ac
commit
dd85644fca
8 changed files with 163 additions and 41 deletions
|
|
@ -295,9 +295,9 @@ function MobileNav({
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Profile / Login tab */}
|
{/* Profile / Login tab — goes straight to profile section when logged in */}
|
||||||
<Link
|
<Link
|
||||||
to="/settings"
|
to={isLoggedIn ? "/settings?section=profile" : "/settings"}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex flex-col items-center justify-center gap-0.5 py-1.5 px-1 rounded-xl transition-all duration-150 flex-1 touch-manipulation active:scale-90',
|
'relative flex flex-col items-center justify-center gap-0.5 py-1.5 px-1 rounded-xl transition-all duration-150 flex-1 touch-manipulation active:scale-90',
|
||||||
isActive('/settings') ? 'text-[#ff0040]' : 'text-gray-500'
|
isActive('/settings') ? 'text-[#ff0040]' : 'text-gray-500'
|
||||||
|
|
|
||||||
|
|
@ -59,9 +59,11 @@ function LatestDropPill() {
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
// Hide on newsletter page itself, if too old, or if already dismissed for this issue
|
// Hide on newsletter page, map page (to avoid conflicts with map UI), if too old, or if already dismissed
|
||||||
|
const isMapPage = location.pathname === '/' || location.pathname === '/map';
|
||||||
if (
|
if (
|
||||||
location.pathname === '/newsletter' ||
|
location.pathname === '/newsletter' ||
|
||||||
|
isMapPage ||
|
||||||
age > PILL_VISIBLE_DAYS ||
|
age > PILL_VISIBLE_DAYS ||
|
||||||
dismissed === latest.number
|
dismissed === latest.number
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ function StatCard({
|
||||||
trustedValue,
|
trustedValue,
|
||||||
color,
|
color,
|
||||||
loading,
|
loading,
|
||||||
|
thisMonthDelta,
|
||||||
}: {
|
}: {
|
||||||
icon: React.ComponentType<{ className?: string }>;
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
value: number;
|
value: number;
|
||||||
|
|
@ -59,6 +60,8 @@ function StatCard({
|
||||||
trustedValue?: number;
|
trustedValue?: number;
|
||||||
color: string;
|
color: string;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
/** How many new items were added this month */
|
||||||
|
thisMonthDelta?: number;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Card className="bg-black/80 border" style={{ borderColor: `${color}33` }}>
|
<Card className="bg-black/80 border" style={{ borderColor: `${color}33` }}>
|
||||||
|
|
@ -69,7 +72,18 @@ function StatCard({
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Skeleton className="h-10 w-20 mb-1" />
|
<Skeleton className="h-10 w-20 mb-1" />
|
||||||
) : (
|
) : (
|
||||||
|
<div className="flex items-end gap-2 flex-wrap">
|
||||||
<p className="text-4xl font-black leading-none" style={{ color }}>{value.toLocaleString()}</p>
|
<p className="text-4xl font-black leading-none" style={{ color }}>{value.toLocaleString()}</p>
|
||||||
|
{typeof thisMonthDelta === 'number' && thisMonthDelta > 0 && (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-0.5 text-[10px] font-black uppercase tracking-wide px-1.5 py-0.5 rounded-full mb-0.5"
|
||||||
|
style={{ background: `${color}22`, color }}
|
||||||
|
>
|
||||||
|
<TrendingUp className="h-2.5 w-2.5" />
|
||||||
|
+{thisMonthDelta} this month
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{!loading && trustedValue !== undefined && (
|
{!loading && trustedValue !== undefined && (
|
||||||
<p className="text-xs mt-1.5 flex items-center gap-1" style={{ color: '#555' }}>
|
<p className="text-xs mt-1.5 flex items-center gap-1" style={{ color: '#555' }}>
|
||||||
|
|
@ -155,6 +169,9 @@ export default function CommunityStatsPage() {
|
||||||
const recentMonthly = useMemo(() => stats?.monthly.slice(-12) ?? [], [stats]);
|
const recentMonthly = useMemo(() => stats?.monthly.slice(-12) ?? [], [stats]);
|
||||||
const recentCumulative = useMemo(() => stats?.cumulativeSpots.slice(-12) ?? [], [stats]);
|
const recentCumulative = useMemo(() => stats?.cumulativeSpots.slice(-12) ?? [], [stats]);
|
||||||
|
|
||||||
|
// Current month delta — how many items were added this month
|
||||||
|
const thisMonth = useMemo(() => stats?.monthly[stats.monthly.length - 1], [stats]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="container max-w-6xl px-4 py-10 md:py-14">
|
<div className="container max-w-6xl px-4 py-10 md:py-14">
|
||||||
|
|
@ -193,16 +210,16 @@ export default function CommunityStatsPage() {
|
||||||
<TrendingUp className="h-3.5 w-3.5" /> Community at a Glance
|
<TrendingUp className="h-3.5 w-3.5" /> Community at a Glance
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||||
<StatCard icon={MapPin} value={stats?.totalSpots ?? 0} label="Skate Spots" color="#ff0040" loading={isLoading} trustedValue={stats?.trustedSpots} />
|
<StatCard icon={MapPin} value={stats?.totalSpots ?? 0} label="Skate Spots" color="#ff0040" loading={isLoading} trustedValue={stats?.trustedSpots} thisMonthDelta={thisMonth?.spots} />
|
||||||
<StatCard icon={Store} value={stats?.totalShops ?? 0} label="Listed Shops" color="#4ade80" loading={isLoading} />
|
<StatCard icon={Store} value={stats?.totalShops ?? 0} label="Listed Shops" color="#4ade80" loading={isLoading} thisMonthDelta={thisMonth?.shops} />
|
||||||
<StatCard icon={Calendar} value={stats?.totalEvents ?? 0} label="Events" color="#ccff00" loading={isLoading} />
|
<StatCard icon={Calendar} value={stats?.totalEvents ?? 0} label="Events" color="#ccff00" loading={isLoading} thisMonthDelta={thisMonth?.events} />
|
||||||
<StatCard icon={Users} value={stats?.totalMappers ?? 0} label="Mappers" color="#facc15" loading={isLoading} trustedValue={stats?.trustedMappers} sub="unique contributors" />
|
<StatCard icon={Users} value={stats?.totalMappers ?? 0} label="Mappers" color="#facc15" loading={isLoading} trustedValue={stats?.trustedMappers} sub="unique contributors" thisMonthDelta={thisMonth?.activeMappers} />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mt-3">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mt-3">
|
||||||
<StatCard icon={GraduationCap} value={stats?.totalCoaches ?? 0} label="Coaches" color="#ff6600" loading={isLoading} />
|
<StatCard icon={GraduationCap} value={stats?.totalCoaches ?? 0} label="Coaches" color="#ff6600" loading={isLoading} thisMonthDelta={thisMonth?.coaches} />
|
||||||
<StatCard icon={Video} value={stats?.totalFilmers ?? 0} label="Filmers" color="#60a5fa" loading={isLoading} />
|
<StatCard icon={Video} value={stats?.totalFilmers ?? 0} label="Filmers" color="#60a5fa" loading={isLoading} thisMonthDelta={thisMonth?.filmers} />
|
||||||
<StatCard icon={Scissors} value={stats?.totalEditors ?? 0} label="Editors" color="#c084fc" loading={isLoading} />
|
<StatCard icon={Scissors} value={stats?.totalEditors ?? 0} label="Editors" color="#c084fc" loading={isLoading} thisMonthDelta={thisMonth?.editors} />
|
||||||
<StatCard icon={Route} value={stats?.totalTours ?? 0} label="Tours" color="#f472b6" loading={isLoading} />
|
<StatCard icon={Route} value={stats?.totalTours ?? 0} label="Tours" color="#f472b6" loading={isLoading} thisMonthDelta={thisMonth?.tours} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -700,6 +700,9 @@ function EmptyState({
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
) : null}
|
) : null}
|
||||||
|
<Link to="/faq" className="mt-5 text-xs text-gray-600 hover:text-gray-400 underline underline-offset-2 transition-colors">
|
||||||
|
Have questions? Check the FAQ →
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -411,15 +411,34 @@ export default function GearSwapPage() {
|
||||||
<h3 className="text-xl font-bold text-white mb-2">No listings found</h3>
|
<h3 className="text-xl font-bold text-white mb-2">No listings found</h3>
|
||||||
<p className="text-gray-400 mb-6 max-w-md mx-auto">
|
<p className="text-gray-400 mb-6 max-w-md mx-auto">
|
||||||
{searchQuery || selectedCategory !== 'all' || trustFilter !== 'all'
|
{searchQuery || selectedCategory !== 'all' || trustFilter !== 'all'
|
||||||
? 'Try adjusting your filters or search query.'
|
? 'No gear matches your current filters. Try clearing them or searching something else.'
|
||||||
: 'Be the first to list your gear!'}
|
: 'Be the first to list your gear — decks, trucks, wheels, shoes, anything skate!'}
|
||||||
</p>
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
|
||||||
|
{(searchQuery || selectedCategory !== 'all' || trustFilter !== 'all') && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="gap-2 border-orange-500/40 text-orange-400 hover:bg-orange-500/10"
|
||||||
|
onClick={() => {
|
||||||
|
setSearchQuery('');
|
||||||
|
setSelectedCategory('all');
|
||||||
|
setTrustFilter('all');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Filter className="h-4 w-4" />
|
||||||
|
Clear Filters
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Link to="/create-listing">
|
<Link to="/create-listing">
|
||||||
<Button className="gap-2 bg-orange-500 hover:bg-orange-600">
|
<Button className="gap-2 bg-orange-500 hover:bg-orange-600">
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
List Your Gear
|
List Your Gear
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
</div>
|
||||||
|
<Link to="/faq" className="mt-5 inline-block text-xs text-gray-600 hover:text-gray-400 underline underline-offset-2 transition-colors">
|
||||||
|
Have questions? Check the FAQ →
|
||||||
|
</Link>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -744,15 +744,22 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
|
||||||
<Icon className="h-8 w-8 text-gray-600" />
|
<Icon className="h-8 w-8 text-gray-600" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-black mb-2 text-white">{c.title}</h3>
|
<h3 className="text-lg font-black mb-2 text-white">{c.title}</h3>
|
||||||
<p className="text-gray-500 text-sm max-w-xs mb-6">{c.body}</p>
|
<p className="text-gray-500 text-sm max-w-xs mb-4">{c.body}</p>
|
||||||
{user && (
|
{user ? (
|
||||||
<Link to={c.to}>
|
<Link to={c.to}>
|
||||||
<button className={cn('flex items-center gap-2 px-5 py-2.5 rounded-xl font-black text-sm uppercase tracking-wide text-white transition-all active:scale-95', c.color)}>
|
<button className={cn('flex items-center gap-2 px-5 py-2.5 rounded-xl font-black text-sm uppercase tracking-wide text-white transition-all active:scale-95', c.color)}>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
{c.cta}
|
{c.cta}
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-gray-600 mb-4">
|
||||||
|
<Link to="/settings" className="text-[#ccff00] underline underline-offset-2">Log in</Link> to create a profile
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
|
<Link to="/faq" className="mt-4 text-xs text-gray-600 hover:text-gray-400 underline underline-offset-2 transition-colors">
|
||||||
|
Have questions? Check the FAQ →
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import { useComments } from '@/hooks/useComments';
|
||||||
import { useStash, STASH_CATEGORIES, CONDITION_LABELS, RARITY_TIERS, type StashItem } from '@/hooks/useStash';
|
import { useStash, STASH_CATEGORIES, CONDITION_LABELS, RARITY_TIERS, type StashItem } from '@/hooks/useStash';
|
||||||
import { useAuthor } from '@/hooks/useAuthor';
|
import { useAuthor } from '@/hooks/useAuthor';
|
||||||
import { useSatsToUsd } from '@/hooks/useExchangeRate';
|
import { useSatsToUsd } from '@/hooks/useExchangeRate';
|
||||||
|
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
||||||
import { genUserName } from '@/lib/genUserName';
|
import { genUserName } from '@/lib/genUserName';
|
||||||
import { useTranslation } from '@/contexts/LanguageContext';
|
import { useTranslation } from '@/contexts/LanguageContext';
|
||||||
import { SPOT_CATEGORIES, SPOT_TYPE_LABELS } from '@/lib/types';
|
import { SPOT_CATEGORIES, SPOT_TYPE_LABELS } from '@/lib/types';
|
||||||
|
|
@ -326,37 +327,53 @@ function StashDrawer() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Universal directions picker — works on GrapheneOS, iOS, any browser
|
// Detect iOS/macOS for smarter maps ordering
|
||||||
|
function isApplePlatform(): boolean {
|
||||||
|
if (typeof navigator === 'undefined') return false;
|
||||||
|
return /iPhone|iPad|iPod|Macintosh/.test(navigator.userAgent) && !/(Windows|Android)/.test(navigator.userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Universal directions picker — works on iOS, Android, desktop
|
||||||
function DirectionsMenu({ lat, lon, label }: { lat: string; lon: string; label: string }) {
|
function DirectionsMenu({ lat, lon, label }: { lat: string; lon: string; label: string }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const onApple = isApplePlatform();
|
||||||
|
|
||||||
const options = [
|
const allOptions = [
|
||||||
{
|
{
|
||||||
name: 'Maps (device default)',
|
name: 'Apple Maps',
|
||||||
sub: 'Opens your default maps app',
|
sub: 'iOS & macOS',
|
||||||
href: `geo:${lat},${lon}?q=${lat},${lon}(${label})`,
|
href: `https://maps.apple.com/?ll=${lat},${lon}&q=${label}&dirflg=d`,
|
||||||
emoji: '📍',
|
emoji: '🍎',
|
||||||
},
|
appleOnly: true,
|
||||||
{
|
|
||||||
name: 'OpenStreetMap',
|
|
||||||
sub: 'Privacy-friendly, no account',
|
|
||||||
href: `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}&zoom=17`,
|
|
||||||
emoji: '🗺️',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Google Maps',
|
name: 'Google Maps',
|
||||||
sub: 'Opens in browser or app',
|
sub: 'Opens in browser or app',
|
||||||
href: `https://maps.google.com/?q=${lat},${lon}`,
|
href: `https://maps.google.com/?q=${lat},${lon}`,
|
||||||
emoji: '🔵',
|
emoji: '🔵',
|
||||||
|
appleOnly: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Apple Maps',
|
name: 'OpenStreetMap',
|
||||||
sub: 'iOS & macOS',
|
sub: 'Privacy-friendly, no account',
|
||||||
href: `https://maps.apple.com/?ll=${lat},${lon}&q=${label}`,
|
href: `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}&zoom=17`,
|
||||||
emoji: '🍎',
|
emoji: '🗺️',
|
||||||
|
appleOnly: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Waze',
|
||||||
|
sub: 'Community-powered navigation',
|
||||||
|
href: `https://waze.com/ul?ll=${lat},${lon}&navigate=yes`,
|
||||||
|
emoji: '🔸',
|
||||||
|
appleOnly: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// On Apple platforms: Apple Maps first. On others: Google first, no Apple.
|
||||||
|
const options = onApple
|
||||||
|
? allOptions
|
||||||
|
: allOptions.filter(o => !o.appleOnly);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -372,8 +389,8 @@ function DirectionsMenu({ lat, lon, label }: { lat: string; lon: string; label:
|
||||||
<>
|
<>
|
||||||
{/* Backdrop */}
|
{/* Backdrop */}
|
||||||
<div className="fixed inset-0 z-[2000]" onClick={() => setOpen(false)} />
|
<div className="fixed inset-0 z-[2000]" onClick={() => setOpen(false)} />
|
||||||
{/* Menu */}
|
{/* Menu — fixed so it never clips behind the Sheet stacking context */}
|
||||||
<div className="absolute bottom-14 right-0 z-[2001] bg-black/95 border border-white/15 rounded-2xl shadow-2xl overflow-hidden w-64 backdrop-blur">
|
<div className="fixed bottom-[30vh] left-1/2 -translate-x-1/2 z-[2001] bg-black/97 border border-white/15 rounded-2xl shadow-2xl overflow-hidden w-72 backdrop-blur">
|
||||||
<div className="px-4 py-3 border-b border-white/8">
|
<div className="px-4 py-3 border-b border-white/8">
|
||||||
<p className="text-xs font-black uppercase tracking-widest text-white/50">Open with</p>
|
<p className="text-xs font-black uppercase tracking-widest text-white/50">Open with</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -550,6 +567,10 @@ export default function MapPage() {
|
||||||
// Mobile filters sheet
|
// Mobile filters sheet
|
||||||
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
|
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
|
||||||
|
|
||||||
|
// Welcome banner — show once to new/logged-out visitors, dismissible
|
||||||
|
const [welcomeDismissed, setWelcomeDismissed] = useLocalStorage('skatespots_welcome_dismissed', false);
|
||||||
|
const showWelcomeBanner = !user && !welcomeDismissed;
|
||||||
|
|
||||||
// Quick camera capture
|
// Quick camera capture
|
||||||
const [showQuickCamera, setShowQuickCamera] = useState(false);
|
const [showQuickCamera, setShowQuickCamera] = useState(false);
|
||||||
const { mutateAsync: uploadFile, isPending: isUploadingQuickPhoto } = useUploadFile();
|
const { mutateAsync: uploadFile, isPending: isUploadingQuickPhoto } = useUploadFile();
|
||||||
|
|
@ -1279,6 +1300,32 @@ export default function MapPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Welcome Banner — shown once to logged-out visitors */}
|
||||||
|
{showWelcomeBanner && (
|
||||||
|
<div className="relative z-10 bg-gradient-to-r from-[#ff0040]/90 via-black/95 to-[#ccff00]/20 border-b border-[#ff0040]/40 backdrop-blur">
|
||||||
|
<div className="container px-4 py-2.5 flex items-center gap-3">
|
||||||
|
<span className="text-xl flex-shrink-0">🛹</span>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-black text-white uppercase tracking-wide leading-tight">New here?</p>
|
||||||
|
<p className="text-xs text-white/70 leading-tight">Discover skate spots, events & the community.</p>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
to="/onboarding"
|
||||||
|
className="flex-shrink-0 px-3 py-1.5 rounded-full bg-[#ccff00] text-black text-xs font-black uppercase tracking-wide hover:bg-[#ccff00]/90 active:scale-95 transition-transform"
|
||||||
|
>
|
||||||
|
Get Started
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={() => setWelcomeDismissed(true)}
|
||||||
|
className="flex-shrink-0 w-7 h-7 flex items-center justify-center rounded-full hover:bg-white/10 text-white/40 hover:text-white transition-colors"
|
||||||
|
aria-label="Dismiss"
|
||||||
|
>
|
||||||
|
<X className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1 relative">
|
||||||
{viewMode === 'map' ? (
|
{viewMode === 'map' ? (
|
||||||
|
|
@ -1322,6 +1369,29 @@ export default function MapPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Add Spot FAB — primary CTA, bottom-left, always visible on map */}
|
||||||
|
<div className={cn(
|
||||||
|
"absolute bottom-4 left-4 z-[1000] transition-opacity duration-150 md:hidden",
|
||||||
|
mobileFiltersOpen || selectedSpot ? "opacity-0 pointer-events-none" : "opacity-100"
|
||||||
|
)}>
|
||||||
|
{user ? (
|
||||||
|
<Link to="/create-spot">
|
||||||
|
<button className="flex items-center gap-2 h-12 pl-3 pr-4 rounded-full bg-[#ff0040] shadow-xl shadow-[#ff0040]/40 border-2 border-white/20 text-white font-black text-sm uppercase tracking-wide active:scale-95 transition-transform">
|
||||||
|
<Plus className="h-5 w-5 flex-shrink-0" />
|
||||||
|
Add Spot
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => document.querySelector<HTMLElement>('[data-login-trigger]')?.click()}
|
||||||
|
className="flex items-center gap-2 h-12 pl-3 pr-4 rounded-full bg-[#ff0040]/80 shadow-xl shadow-[#ff0040]/30 border-2 border-white/20 text-white font-black text-sm uppercase tracking-wide active:scale-95 transition-transform backdrop-blur"
|
||||||
|
>
|
||||||
|
<Plus className="h-5 w-5 flex-shrink-0" />
|
||||||
|
Add Spot
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Near Me button + attribution — floating top-right, hidden when any overlay is open */}
|
{/* Near Me button + attribution — floating top-right, hidden when any overlay is open */}
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"absolute top-4 right-4 z-[1000] flex flex-col items-end gap-1.5 transition-opacity duration-150",
|
"absolute top-4 right-4 z-[1000] flex flex-col items-end gap-1.5 transition-opacity duration-150",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useSeoMeta } from '@unhead/react';
|
import { useSeoMeta } from '@unhead/react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
import { Layout } from '@/components/Layout';
|
import { Layout } from '@/components/Layout';
|
||||||
import { EditProfileForm } from '@/components/EditProfileForm';
|
import { EditProfileForm } from '@/components/EditProfileForm';
|
||||||
import { RelayListManager } from '@/components/RelayListManager';
|
import { RelayListManager } from '@/components/RelayListManager';
|
||||||
|
|
@ -502,7 +502,11 @@ export default function SettingsPage() {
|
||||||
description: 'Manage your SkateSpots account settings.',
|
description: 'Manage your SkateSpots account settings.',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [section, setSection] = useState<Section>(null);
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
// Allow deep-linking into a specific section via ?section=profile etc.
|
||||||
|
const initialSection = (searchParams.get('section') as Section) ?? null;
|
||||||
|
const [section, setSection] = useState<Section>(initialSection);
|
||||||
|
|
||||||
const handleNavigate = (s: Section) => setSection(s);
|
const handleNavigate = (s: Section) => setSection(s);
|
||||||
const handleBack = () => setSection(null);
|
const handleBack = () => setSection(null);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue