SkateSpots/src/components/GearSwapItemCard.tsx
shakespeare.diy e29d35f1e4 fix: restore full skate-spots source tree from misplaced src/src/ copy
The entire skate-spots codebase was accidentally copied into src/src/ and
public/public/ instead of src/ and public/ directly. This left the real
src/ directory as blank MKStack template stubs, explaining why the landing
page showed the default template instead of SkateSpots.

Changes made:
- Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx,
  all pages, components, hooks, contexts, lib, i18n, test)
- Copied all files from public/public/ up to public/ (images, manifest, etc.)
- Deleted src/src/ and public/public/ nested directories
- Removed HelloWorld.tsx template stub from pages/
- Retained the previously fixed index.css (Tailwind v3 directives)

Full skate-spots feature set is now active:
- All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.)
- NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler
- Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.)
- Outfit Variable font, Amber pre-check in main.tsx
- All hooks (useSkateSpots, useCoaches, useZaps, etc.)
- i18n support (en/es)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-15 22:18:28 +07:00

296 lines
11 KiB
TypeScript

import { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { nip19 } from 'nostr-tools';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { useAuthor } from '@/hooks/useAuthor';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { genUserName } from '@/lib/genUserName';
import { getSkateboardRating, getTrustLevel, SELLER_TRUST_BADGES } from '@/lib/skateboardRating';
import { GEAR_CONDITIONS, GEAR_CATEGORIES, PRICE_TYPES, formatPrice, type MarketplaceItem } from '@/lib/types';
import {
MessageCircle,
MapPin,
Search,
Tag,
CheckCircle,
ShieldCheck,
AlertTriangle,
MoreVertical,
Flag,
} from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { ReportDialog } from '@/components/ReportDialog';
import { formatDistanceToNow } from 'date-fns';
interface GearSwapItemCardProps {
item: MarketplaceItem;
trustScore?: number;
salesCount: number;
isFollowing?: boolean;
}
export function GearSwapItemCard({ item, trustScore, salesCount, isFollowing }: GearSwapItemCardProps) {
const { user } = useCurrentUser();
const navigate = useNavigate();
const author = useAuthor(item.pubkey);
const metadata = author.data?.metadata;
const displayName = metadata?.name ?? genUserName(item.pubkey);
const avatar = metadata?.picture;
const [showReportDialog, setShowReportDialog] = useState(false);
const npub = nip19.npubEncode(item.pubkey);
const isOwnListingCheck = user?.pubkey === item.pubkey;
// Format price display
const priceDisplay = formatPrice(item.price, item.priceType);
const timeAgo = formatDistanceToNow(new Date(item.createdAt * 1000), { addSuffix: true });
const primaryImage = item.images[0];
const isWTB = item.listingType === 'wtb';
// Get condition info
const conditionInfo = item.condition ? GEAR_CONDITIONS[item.condition] : null;
// Get category label
const categoryLabel = item.category?.find((c) => GEAR_CATEGORIES[c as keyof typeof GEAR_CATEGORIES])
? GEAR_CATEGORIES[item.category.find((c) => GEAR_CATEGORIES[c as keyof typeof GEAR_CATEGORIES]) as keyof typeof GEAR_CATEGORIES]
: null;
// Get price type label
const priceTypeLabel = item.priceType ? PRICE_TYPES[item.priceType]?.label : null;
// Skateboard rating for seller
const sellerRating = getSkateboardRating(salesCount);
// Trust level
const trustLevel = getTrustLevel(trustScore);
const trustBadge = SELLER_TRUST_BADGES[trustLevel];
const isOwnListing = user?.pubkey === item.pubkey;
return (
<Card
className={`bg-black/90 border-2 group cursor-pointer transition-all hover:scale-[1.02] ${
isWTB
? 'border-blue-500/20 hover:border-blue-500/60 hover:shadow-[0_0_30px_rgba(59,130,246,0.2)]'
: 'border-orange-500/20 hover:border-orange-500/60 hover:shadow-[0_0_30px_rgba(255,102,0,0.2)]'
}`}
onClick={() => navigate(`/messages/${npub}`)}
>
<CardContent className="p-0">
{/* Image */}
<div className="relative aspect-square overflow-hidden rounded-t-lg">
{primaryImage ? (
<img
src={primaryImage}
alt={item.title}
className="w-full h-full object-cover transition-transform group-hover:scale-105"
/>
) : (
<div
className={`w-full h-full flex items-center justify-center ${
isWTB ? 'bg-blue-950/50' : 'bg-orange-950/50'
}`}
>
<span className="text-6xl">{isWTB ? '🔍' : '🛹'}</span>
</div>
)}
{/* WTB Badge */}
{isWTB && (
<Badge className="absolute top-2 left-2 bg-blue-500 text-white border-0 shadow-lg">
<Search className="h-3 w-3 mr-1" />
WTB
</Badge>
)}
{/* Price Badge */}
<div className="absolute top-2 right-2">
<Badge
className={`text-sm font-bold border-0 shadow-lg ${
item.priceType === 'free'
? 'bg-green-500 text-white'
: isWTB
? 'bg-blue-500 text-white'
: 'bg-orange-500 text-white'
}`}
>
{priceDisplay}
</Badge>
</div>
{/* Condition Badge (bottom left) */}
{conditionInfo && (
<Badge
variant="outline"
className="absolute bottom-2 left-2 bg-black/80 border-gray-600 text-xs backdrop-blur-sm"
>
{conditionInfo.emoji} {conditionInfo.label}
</Badge>
)}
{/* More options menu */}
{!isOwnListingCheck && (
<div className="absolute bottom-2 right-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-7 w-7 bg-black/80 backdrop-blur-sm text-gray-400 hover:text-white"
onClick={(e) => e.stopPropagation()}
>
<MoreVertical className="h-3.5 w-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
setShowReportDialog(true);
}}
className="text-red-500 focus:text-red-500"
>
<Flag className="h-4 w-4 mr-2" />
Report
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</div>
{/* Content */}
<div className="p-4">
{/* Title */}
<h3
className={`font-bold text-sm uppercase mb-1 line-clamp-1 ${
isWTB ? 'text-blue-400' : 'text-orange-400'
}`}
>
{item.title}
</h3>
{/* Category + Size */}
<div className="flex flex-wrap gap-1 mb-3">
{categoryLabel && (
<Badge variant="outline" className="text-[10px] border-gray-700 text-gray-400">
{categoryLabel}
</Badge>
)}
{item.size && (
<Badge variant="outline" className="text-[10px] border-gray-700 text-gray-400">
{item.size}
</Badge>
)}
</div>
{/* Location */}
{item.location && (
<div className="flex items-center gap-1 text-xs text-gray-500 mb-3">
<MapPin className="h-3 w-3 text-orange-500" />
<span className="truncate">{item.location}</span>
</div>
)}
{/* Divider */}
<div className="border-t border-gray-800 pt-3">
{/* Seller Info */}
<div className="flex items-center gap-2 mb-2">
<Avatar className="h-6 w-6 border border-gray-700">
<AvatarImage src={avatar} alt={displayName} />
<AvatarFallback className="bg-gray-800 text-gray-400 text-xs">
{displayName.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<span className="text-xs text-gray-400 truncate flex-1">{displayName}</span>
{/* Trust Badge */}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Badge
variant="outline"
className={`text-[10px] px-1.5 gap-0.5 ${trustBadge.className}`}
>
{trustLevel === 'verified' && <CheckCircle className="h-3 w-3" />}
{trustLevel === 'trusted' && <ShieldCheck className="h-3 w-3" />}
{trustLevel === 'new' && <AlertTriangle className="h-3 w-3" />}
{trustBadge.label}
</Badge>
</TooltipTrigger>
<TooltipContent side="top">
<p className="text-xs">{trustBadge.description}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{/* Skateboard Rating */}
<div className="flex items-center justify-between">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-1">
{sellerRating.rating > 0 ? (
<span className="text-sm">{sellerRating.display}</span>
) : (
<span className="text-xs text-gray-500">🛹 New seller</span>
)}
</div>
</TooltipTrigger>
<TooltipContent side="bottom">
<div className="text-center">
<p className="font-bold">{salesCount} sale{salesCount !== 1 ? 's' : ''}</p>
<p className="text-xs text-muted-foreground">{sellerRating.label}</p>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<span className="text-[10px] text-gray-600">{timeAgo}</span>
</div>
</div>
{/* Contact Button */}
<Button
variant="outline"
size="sm"
className={`w-full mt-3 gap-2 ${
isWTB
? 'border-blue-500/50 text-blue-400 hover:bg-blue-500/10'
: 'border-orange-500/50 text-orange-400 hover:bg-orange-500/10'
}`}
onClick={(e) => {
e.stopPropagation();
navigate(`/messages/${npub}`);
}}
>
<MessageCircle className="h-4 w-4" />
{isWTB ? 'I Have This!' : 'Contact Seller'}
</Button>
</div>
</CardContent>
{/* Report Dialog */}
<ReportDialog
open={showReportDialog}
onOpenChange={setShowReportDialog}
targetType="listing"
targetPubkey={item.pubkey}
targetEventId={item.id}
targetTitle={item.title}
/>
</Card>
);
}