fix(ui): owner spot delete, mobile back and simplified account menu
This commit is contained in:
parent
b0df599b52
commit
28d34e3fbe
2 changed files with 51 additions and 41 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue