fix(ui): owner spot delete, mobile back and simplified account menu

This commit is contained in:
SkateSpots 2026-10-30 21:21:00 +07:00
parent b0df599b52
commit 28d34e3fbe
2 changed files with 51 additions and 41 deletions

View file

@ -1,5 +1,5 @@
import { useState, useRef, useEffect, lazy, Suspense } from 'react'; import { useState, useRef, useEffect, lazy, Suspense } from 'react';
import { Link, Outlet, useLocation } from 'react-router-dom'; import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { isNativePlatform } from '@/lib/platform'; import { isNativePlatform } from '@/lib/platform';
@ -39,7 +39,6 @@ import {
User, User,
Plus, Plus,
CheckCircle2, CheckCircle2,
Database,
Package, Package,
Heart, Heart,
CreditCard, CreditCard,
@ -53,6 +52,7 @@ import {
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
ChevronDown, ChevronDown,
ArrowLeft,
Search, Search,
} from 'lucide-react'; } from 'lucide-react';
import { OfflineIndicator } from '@/components/OfflineIndicator'; import { OfflineIndicator } from '@/components/OfflineIndicator';
@ -283,12 +283,6 @@ function MobileHeaderUserMenu({
<span className="text-green-400 font-bold">{t('chromeUi.setStatus')}</span> <span className="text-green-400 font-bold">{t('chromeUi.setStatus')}</span>
<span className="ml-auto text-xs text-green-500">🛹</span> <span className="ml-auto text-xs text-green-500">🛹</span>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/create-spot" className="flex items-center gap-2 cursor-pointer">
<Plus className="h-4 w-4 text-[#ccff00]" />
<span className="text-[#ccff00] font-medium">{t('chromeUi.addSpot')}</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link to={`/${nip19.npubEncode(pubkey)}`} className="flex items-center gap-2 cursor-pointer"> <Link to={`/${nip19.npubEncode(pubkey)}`} className="flex items-center gap-2 cursor-pointer">
<User className="h-4 w-4" /> <User className="h-4 w-4" />
@ -301,36 +295,6 @@ function MobileHeaderUserMenu({
{t('nav.settings')} {t('nav.settings')}
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/settings?section=mydata" className="flex items-center gap-2 cursor-pointer">
<Database className="h-4 w-4 text-[#ccff00]" />
<span className="text-[#ccff00]">{t('chromeUi.myData')}</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/stash" className="flex items-center gap-2 cursor-pointer">
<Package className="h-4 w-4 text-purple-400" />
<span className="text-purple-400">{t('chromeUi.myStash')}</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/wallet" className="flex items-center gap-2 cursor-pointer">
<Wallet className="h-4 w-4 text-orange-400" />
<span className="text-orange-400">{t('nav.wallet')}</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/leaderboards" className="flex items-center gap-2 cursor-pointer">
<BarChart3 className="h-4 w-4 text-[#ccff00]" />
<span className="text-[#ccff00]">{t('nav.leaderboards')}</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/marketplace" className="flex items-center gap-2 cursor-pointer">
<Package className="h-4 w-4 text-orange-400" />
<span className="text-orange-400">{t('nav.marketplace')}</span>
</Link>
</DropdownMenuItem>
{/* ── Accounts: switch between logged-in npubs + add another ── */} {/* ── Accounts: switch between logged-in npubs + add another ── */}
<DropdownMenuSeparator /> <DropdownMenuSeparator />
@ -512,6 +476,8 @@ function MobileNav({
export function Layout({ children, noPadding }: LayoutProps) { export function Layout({ children, noPadding }: LayoutProps) {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate();
const showMobileBack = location.pathname !== '/' && location.pathname !== '/map';
// The map is full-bleed (it manages its own height), so it gets no bottom // The map is full-bleed (it manages its own height), so it gets no bottom
// padding even though it shares this one persistent layout with every route. // padding even though it shares this one persistent layout with every route.
const isFullBleed = noPadding || location.pathname === '/' || location.pathname === '/map'; const isFullBleed = noPadding || location.pathname === '/' || location.pathname === '/map';
@ -842,6 +808,22 @@ export function Layout({ children, noPadding }: LayoutProps) {
{/* Mobile Header */} {/* Mobile Header */}
<header className="md:hidden sticky top-0 z-50 border-b border-red-500/30 bg-black/95 backdrop-blur safe-area-top"> <header className="md:hidden sticky top-0 z-50 border-b border-red-500/30 bg-black/95 backdrop-blur safe-area-top">
<div className="flex items-center justify-between h-14 px-3 gap-3"> <div className="flex items-center justify-between h-14 px-3 gap-3">
{showMobileBack && (
<Button
type="button"
variant="ghost"
size="icon"
className="-ml-1 h-9 w-9 shrink-0 text-gray-300 hover:bg-white/5 hover:text-white"
aria-label={t('common.back')}
title={t('common.back')}
onClick={() => {
if (location.key !== 'default') navigate(-1);
else navigate('/');
}}
>
<ArrowLeft className="h-5 w-5" />
</Button>
)}
<Link <Link
to={user ? `/${nip19.npubEncode(user.pubkey)}` : '/'} to={user ? `/${nip19.npubEncode(user.pubkey)}` : '/'}
className="flex items-center gap-2 touch-manipulation active:opacity-80 flex-1 min-w-0" className="flex items-center gap-2 touch-manipulation active:opacity-80 flex-1 min-w-0"
@ -853,7 +835,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
alt="SkateSpots" alt="SkateSpots"
className="w-9 h-9 rounded-lg flex-shrink-0 shadow-lg shadow-red-500/30" className="w-9 h-9 rounded-lg flex-shrink-0 shadow-lg shadow-red-500/30"
/> />
<span className="min-w-0 truncate font-display text-lg neon-red tracking-wide" style={{ <span className={cn('min-w-0 truncate font-display text-lg neon-red tracking-wide', showMobileBack && 'hidden')} style={{
textShadow: '0 0 8px #ff0040' textShadow: '0 0 8px #ff0040'
}}>SKATESPOTS</span> }}>SKATESPOTS</span>
</Link> </Link>

View file

@ -11,7 +11,7 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu';
import { MapPin, Store, Bookmark, MoreVertical, Flag, ShieldCheck } from 'lucide-react'; import { MapPin, Store, Bookmark, MoreVertical, Flag, ShieldCheck, Trash2 } from 'lucide-react';
import { useAuthor } from '@/hooks/useAuthor'; import { useAuthor } from '@/hooks/useAuthor';
import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useVenuesWithPasses } from '@/hooks/useRiderCard'; import { useVenuesWithPasses } from '@/hooks/useRiderCard';
@ -23,6 +23,7 @@ import { OfficialBadge } from '@/components/OfficialBadge';
import { AddToCollectionDialog } from '@/components/AddToCollectionDialog'; import { AddToCollectionDialog } from '@/components/AddToCollectionDialog';
import { ReportDialog } from '@/components/ReportDialog'; import { ReportDialog } from '@/components/ReportDialog';
import { MuteMenuItem } from '@/components/MuteMenuItem'; import { MuteMenuItem } from '@/components/MuteMenuItem';
import { DeleteEntityDialog } from '@/components/DeleteEntityDialog';
import { SpotStatusBadge } from '@/components/SpotStatusBadge'; import { SpotStatusBadge } from '@/components/SpotStatusBadge';
import type { SkateSpot } from '@/lib/types'; import type { SkateSpot } from '@/lib/types';
import { ACCESS_TYPE_INFO, SHELTER_INFO, SKATE_SHOP_KIND } from '@/lib/types'; import { ACCESS_TYPE_INFO, SHELTER_INFO, SKATE_SHOP_KIND } from '@/lib/types';
@ -30,6 +31,7 @@ import { spotTypeLabel } from '@/lib/spotTypeLabel';
import { ACCESS_TYPE_KEYS, SHELTER_SHORT_KEYS } from '@/lib/labelKeys'; import { ACCESS_TYPE_KEYS, SHELTER_SHORT_KEYS } from '@/lib/labelKeys';
import type { AggregatedSpotStatus } from '@/hooks/useSpotStatus'; import type { AggregatedSpotStatus } from '@/hooks/useSpotStatus';
import { useTranslation } from '@/contexts/language'; import { useTranslation } from '@/contexts/language';
import { isAdminPubkey } from '@/lib/admins';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
interface SpotCardProps { interface SpotCardProps {
@ -48,10 +50,13 @@ export function SpotCard({ spot, status, community }: SpotCardProps) {
const displayName = metadata?.name ?? genUserName(spot.pubkey); const displayName = metadata?.name ?? genUserName(spot.pubkey);
const [showCollectionDialog, setShowCollectionDialog] = useState(false); const [showCollectionDialog, setShowCollectionDialog] = useState(false);
const [showReportDialog, setShowReportDialog] = useState(false); const [showReportDialog, setShowReportDialog] = useState(false);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
// Determine if this is a shop or a spot // Determine if this is a shop or a spot
const isShop = spot.types.includes('shop'); const isShop = spot.types.includes('shop');
const isOwner = user?.pubkey === spot.pubkey; const isOwner = user?.pubkey === spot.pubkey;
const isAdmin = isAdminPubkey(user?.pubkey);
const canDelete = !!user && (isOwner || isAdmin);
const route = isShop ? `/shop/${spot.pubkey}/${spot.identifier}` : `/spot/${spot.pubkey}/${spot.identifier}`; const route = isShop ? `/shop/${spot.pubkey}/${spot.identifier}` : `/spot/${spot.pubkey}/${spot.identifier}`;
// Shops that actually issue access passes get a badge (one shared, cached query). // Shops that actually issue access passes get a badge (one shared, cached query).
@ -245,7 +250,19 @@ export function SpotCard({ spot, status, community }: SpotCardProps) {
<MoreVertical className="h-4 w-4" /> <MoreVertical className="h-4 w-4" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40"> <DropdownMenuContent align="end" className="w-44">
{canDelete && (
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
setShowDeleteDialog(true);
}}
className="text-red-500 focus:text-red-500"
>
<Trash2 className="mr-2 h-4 w-4" />
{isOwner ? t('common.delete') : t('spotUi.removeAdmin')}
</DropdownMenuItem>
)}
{!isOwner && ( {!isOwner && (
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
@ -275,6 +292,17 @@ export function SpotCard({ spot, status, community }: SpotCardProps) {
/> />
)} )}
{/* Owner/admin delete confirmation */}
{canDelete && (
<DeleteEntityDialog
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
event={spot.event}
entityLabel={isShop ? t('mapUi.skateShop') : t('spotUi.spot')}
entityName={spot.title}
/>
)}
{/* Report Dialog */} {/* Report Dialog */}
<ReportDialog <ReportDialog
open={showReportDialog} open={showReportDialog}