- First-visit language auto-detect (Spanish devices land in es; stored choice wins). - Sasquatch AI replies in the app language (skate trick names kept English). - i18n safety net: scripts/check-i18n.mjs wired into npm test — fails on any t() key missing from en/es or any en/es key drift. - ~1,680 new keys (2,297 total) across mapUi / spotUi / marketUi / labUi and ~65 files wired to t(): Map, spots (detail/create/edit/chat), marketplace, trades, offers, escrow, shipping, The Lab, /become/* forms, coaches, skaters. - Policy: only skate TRICK names stay English; user-generated content untouched.
567 lines
24 KiB
TypeScript
567 lines
24 KiB
TypeScript
import { useState } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
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 { ImageWithFallback } from '@/components/ImageWithFallback';
|
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from '@/components/ui/dropdown-menu';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { Label } from '@/components/ui/label';
|
|
import { useAuthor } from '@/hooks/useAuthor';
|
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
import { useUsdToSats } from '@/hooks/useExchangeRate';
|
|
import { useToast } from '@/hooks/useToast';
|
|
import { useNostrPublish } from '@/hooks/useNostrPublish';
|
|
import { useListingReports } from '@/hooks/useListingReports';
|
|
import { useSellerInfo } from '@/hooks/useSellerInfo';
|
|
import { useTrustScore } from '@/hooks/useTrustScore';
|
|
import { useMarkListingSold } from '@/hooks/useMarkListingSold';
|
|
import { MarkSoldDialog } from '@/components/MarkSoldDialog';
|
|
import { ListingTradeActions } from '@/components/ListingTradeActions';
|
|
import { useTradeReputation } from '@/hooks/useTradeAttestations';
|
|
import { DeleteEntityDialog } from '@/components/DeleteEntityDialog';
|
|
import { genUserName } from '@/lib/genUserName';
|
|
import { getLastLocation } from '@/lib/lastLocation';
|
|
import { itemDistanceKm } from '@/lib/sortOptions';
|
|
import { TrustBadge } from '@/components/TrustBadge';
|
|
import { Clock, Search, Tag, Flag, MoreVertical, ShieldCheck, Calendar, Package, AlertTriangle, CheckCircle, RotateCcw, Trash2, Loader2 } from 'lucide-react';
|
|
import { formatDistanceToNow } from 'date-fns';
|
|
import type { MarketplaceItem } from '@/lib/types';
|
|
import { GEAR_CONDITIONS, GEAR_CATEGORIES, PRICE_TYPES, LISTING_REPORT_REASONS, formatPrice } from '@/lib/types';
|
|
import { useTranslation } from '@/contexts/language';
|
|
|
|
interface MarketplaceItemCardProps {
|
|
item: MarketplaceItem;
|
|
compact?: boolean;
|
|
}
|
|
|
|
export function MarketplaceItemCard({ item, compact = false }: MarketplaceItemCardProps) {
|
|
const { t } = useTranslation();
|
|
const { user } = useCurrentUser();
|
|
// Distance from the viewer's last known location (no GPS call — cache only).
|
|
const viewerLoc = getLastLocation();
|
|
const distanceKm = viewerLoc ? itemDistanceKm(item, viewerLoc) : null;
|
|
const { toast } = useToast();
|
|
const { mutateAsync: publishEvent } = useNostrPublish();
|
|
const { relist, isPending: isUpdatingStatus } = useMarkListingSold();
|
|
const author = useAuthor(item.pubkey);
|
|
const metadata = author.data?.metadata;
|
|
const displayName = metadata?.name ?? genUserName(item.pubkey);
|
|
const avatar = metadata?.picture;
|
|
|
|
// Fetch reports for this listing
|
|
const { data: reportData } = useListingReports(item.id, item.pubkey);
|
|
|
|
// Fetch seller info
|
|
const { data: sellerInfo } = useSellerInfo(item.pubkey);
|
|
|
|
// Fetch trust score
|
|
const { data: trustScore } = useTrustScore(item.pubkey);
|
|
|
|
const [showReportDialog, setShowReportDialog] = useState(false);
|
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
|
const [reportReason, setReportReason] = useState<string>('');
|
|
const [reportDetails, setReportDetails] = useState('');
|
|
const [isReporting, setIsReporting] = useState(false);
|
|
const [showMarkSold, setShowMarkSold] = useState(false);
|
|
|
|
// Trade attestations (Attestations NIP, kind 31871) — see useTradeAttestations.
|
|
const listingRef = { pubkey: item.pubkey, identifier: item.identifier };
|
|
const { data: sellerRep } = useTradeReputation(item.pubkey);
|
|
|
|
// Format price display
|
|
const priceDisplay = item.price
|
|
? `${formatPrice(item.price, item.priceType)}${item.price.frequency ? `/${item.price.frequency}` : ''}`
|
|
: formatPrice(undefined, item.priceType);
|
|
|
|
const timeAgo = formatDistanceToNow(new Date(item.createdAt * 1000), { addSuffix: true });
|
|
|
|
const primaryImage = item.images[0];
|
|
const detailUrl = `/listing/${item.pubkey}/${item.identifier}`;
|
|
const isWTB = item.listingType === 'wtb';
|
|
// Expiration is stored as unix timestamp in seconds
|
|
const isExpired = item.expiration ? item.expiration < Math.floor(Date.now() / 1000) : false;
|
|
|
|
// 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;
|
|
|
|
// Sats estimate from the LIVE BTC/USD rate — never a hardcoded constant
|
|
// (that quoted buyers wildly wrong amounts). Non-USD fiat is left unestimated
|
|
// rather than inventing a number.
|
|
const priceAmount = item.price?.amount ? parseFloat(item.price.amount) : 0;
|
|
const priceCurrency = item.price?.currency || 'USD';
|
|
const usdAsSats = useUsdToSats(priceCurrency === 'USD' ? priceAmount : null);
|
|
const priceInSats = !priceAmount
|
|
? 0
|
|
: priceCurrency === 'sats'
|
|
? Math.round(priceAmount)
|
|
: priceCurrency === 'BTC'
|
|
? Math.round(priceAmount * 100000000)
|
|
: (usdAsSats ?? 0);
|
|
|
|
// Seller verification status
|
|
const isVerifiedSeller = sellerInfo?.hasNip05 || false;
|
|
const isHighTrust = (trustScore || 0) >= 60;
|
|
const accountAgeDays = sellerInfo?.accountAge || 0;
|
|
const previousListings = sellerInfo?.listingCount || 0;
|
|
|
|
// Report status
|
|
const reportCount = reportData?.count || 0;
|
|
const shouldHide = reportData?.shouldHide || false;
|
|
|
|
const handleReport = async () => {
|
|
if (!user || !reportReason) {
|
|
toast({ title: t('marketUi.card.selectReason'), variant: 'destructive' });
|
|
return;
|
|
}
|
|
|
|
setIsReporting(true);
|
|
try {
|
|
// Publish NIP-56 report event (kind 1984)
|
|
await publishEvent({
|
|
kind: 1984,
|
|
content: reportDetails.trim(),
|
|
tags: [
|
|
['e', item.id, '', reportReason],
|
|
['p', item.pubkey],
|
|
['L', 'skate-spots.report'],
|
|
['l', reportReason, 'skate-spots.report'],
|
|
],
|
|
});
|
|
|
|
toast({
|
|
title: t('marketUi.card.reportSubmitted'),
|
|
description: t('marketUi.card.reportSubmittedBody'),
|
|
});
|
|
setShowReportDialog(false);
|
|
setReportReason('');
|
|
setReportDetails('');
|
|
} catch (error) {
|
|
toast({
|
|
variant: 'destructive',
|
|
title: t('marketUi.card.reportFailed'),
|
|
description: error instanceof Error ? error.message : t('common.tryAgain'),
|
|
});
|
|
} finally {
|
|
setIsReporting(false);
|
|
}
|
|
};
|
|
|
|
// Don't render expired or heavily reported listings
|
|
if (isExpired || shouldHide) {
|
|
return null;
|
|
}
|
|
|
|
const isOwnListing = user?.pubkey === item.pubkey;
|
|
const isSold = item.status === 'sold';
|
|
|
|
const handleRelist = async () => {
|
|
try {
|
|
await relist(item.event);
|
|
toast({ title: t('marketUi.card.relisted'), description: t('marketUi.card.relistedBody') });
|
|
} catch (error) {
|
|
toast({
|
|
title: t('marketUi.card.couldNotUpdate'),
|
|
description: error instanceof Error ? error.message : t('marketUi.card.somethingWrong'),
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Card
|
|
className={`bg-black/90 border-2 ${
|
|
isWTB
|
|
? 'border-blue-500/20 hover:border-blue-500/60 hover:shadow-blue-500/10'
|
|
: 'border-orange-500/20 hover:border-orange-500/60 hover:shadow-orange-500/10'
|
|
} hover:shadow-2xl transition-all relative`}
|
|
>
|
|
{/* Report warning badge */}
|
|
{reportCount > 0 && reportCount < 3 && (
|
|
<div className="absolute top-2 right-2 z-10">
|
|
<Badge variant="destructive" className="text-xs gap-1">
|
|
<AlertTriangle className="h-3 w-3" />
|
|
{reportCount > 1 ? t('marketUi.card.reports', { count: reportCount }) : t('marketUi.card.reportCount', { count: reportCount })}
|
|
</Badge>
|
|
</div>
|
|
)}
|
|
|
|
<CardContent className="p-4">
|
|
<div className="flex items-start gap-4">
|
|
{/* Product Image */}
|
|
<Link to={detailUrl} className="flex-shrink-0" aria-label={t('marketUi.card.viewTitle', { title: item.title })}>
|
|
{primaryImage ? (
|
|
<div
|
|
className={`w-20 h-20 rounded-lg overflow-hidden border-2 ${
|
|
isWTB ? 'border-blue-500/30' : 'border-orange-500/30'
|
|
}`}
|
|
>
|
|
<ImageWithFallback src={primaryImage} alt={item.title} className="w-full h-full object-cover" />
|
|
</div>
|
|
) : (
|
|
<div
|
|
className={`w-20 h-20 rounded-lg border-2 border-dashed ${
|
|
isWTB ? 'border-blue-500/30 bg-blue-500/5' : 'border-orange-500/30 bg-orange-500/5'
|
|
} flex items-center justify-center`}
|
|
>
|
|
<span className="text-3xl">{isWTB ? '🔍' : '🛹'}</span>
|
|
</div>
|
|
)}
|
|
</Link>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 min-w-0">
|
|
{/* Listing Type Badge + Title */}
|
|
<div className="flex items-start justify-between gap-2 mb-2">
|
|
<div className="flex-1 min-w-0">
|
|
{isWTB && (
|
|
<Badge className="bg-blue-500/20 text-blue-400 border-blue-500/50 mb-1 text-xs">
|
|
<Search className="h-3 w-3 mr-1" />
|
|
{t('marketUi.listingDetail.wantToBuy')}
|
|
</Badge>
|
|
)}
|
|
<Link to={detailUrl}>
|
|
<h3
|
|
className={`font-black text-base uppercase ${
|
|
isWTB ? 'text-blue-400' : 'text-orange-400'
|
|
} leading-tight`}
|
|
>
|
|
{item.title}
|
|
</h3>
|
|
</Link>
|
|
</div>
|
|
<div className="text-right flex items-start gap-1">
|
|
<div>
|
|
<span
|
|
className={`text-lg font-bold ${
|
|
isWTB ? 'text-blue-500' : 'text-orange-500'
|
|
} whitespace-nowrap`}
|
|
>
|
|
{priceDisplay}
|
|
</span>
|
|
{priceTypeLabel && priceTypeLabel !== 'Fixed Price' && (
|
|
<div className="text-xs text-gray-400">{priceTypeLabel}</div>
|
|
)}
|
|
{priceInSats > 0 && (
|
|
<div className="text-xs text-[#ccff00] whitespace-nowrap">
|
|
≈ {priceInSats.toLocaleString()} {t('common.sats')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Actions dropdown */}
|
|
{user && !isOwnListing && (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon" className="h-6 w-6 text-gray-400">
|
|
<MoreVertical className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem
|
|
onClick={() => setShowReportDialog(true)}
|
|
className="text-red-400 focus:text-red-400"
|
|
>
|
|
<Flag className="h-4 w-4 mr-2" />
|
|
{t('marketUi.card.reportListing')}
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)}
|
|
|
|
{/* Owner management dropdown — mark sold / relist / delete */}
|
|
{user && isOwnListing && (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-6 w-6 text-gray-400"
|
|
disabled={isUpdatingStatus}
|
|
aria-label={t('marketUi.card.manageListing')}
|
|
>
|
|
{isUpdatingStatus ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<MoreVertical className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
{isSold ? (
|
|
<DropdownMenuItem onClick={handleRelist} disabled={isUpdatingStatus}>
|
|
<RotateCcw className="h-4 w-4 mr-2" />
|
|
{t('marketUi.listingDetail.relistActive')}
|
|
</DropdownMenuItem>
|
|
) : (
|
|
<DropdownMenuItem onClick={() => setShowMarkSold(true)} disabled={isUpdatingStatus}>
|
|
<CheckCircle className="h-4 w-4 mr-2" />
|
|
{t('marketUi.listingDetail.markSold')}
|
|
</DropdownMenuItem>
|
|
)}
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
onClick={() => setShowDeleteDialog(true)}
|
|
className="text-red-400 focus:text-red-400"
|
|
>
|
|
<Trash2 className="h-4 w-4 mr-2" />
|
|
{t('marketUi.listingDetail.deleteListing')}
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Condition, Size, Category Badges */}
|
|
<div className="flex flex-wrap gap-1 mb-2">
|
|
{conditionInfo && (
|
|
<Badge variant="outline" className="text-xs border-gray-600 bg-gray-800/50">
|
|
{conditionInfo.emoji} {conditionInfo.label}
|
|
</Badge>
|
|
)}
|
|
{item.size && (
|
|
<Badge variant="outline" className="text-xs border-gray-600 bg-gray-800/50">
|
|
{item.size}
|
|
</Badge>
|
|
)}
|
|
{categoryLabel && (
|
|
<Badge variant="outline" className="text-xs border-gray-600 bg-gray-800/50">
|
|
<Tag className="h-3 w-3 mr-1" />
|
|
{categoryLabel}
|
|
</Badge>
|
|
)}
|
|
{item.fulfilment && (
|
|
<Badge variant="outline" className="text-xs border-gray-600 bg-gray-800/50">
|
|
{item.fulfilment === 'pickup'
|
|
? `📍 ${t('marketUi.create.pickup')}`
|
|
: item.fulfilment === 'ships'
|
|
? `📦 ${t('marketUi.create.ships')}`
|
|
: `📍📦 ${t('marketUi.gearSwap.pickupOrShip')}`}
|
|
</Badge>
|
|
)}
|
|
{distanceKm != null && (
|
|
<Badge variant="outline" className="text-xs border-gray-600 bg-gray-800/50">
|
|
{distanceKm < 1
|
|
? `${Math.round(distanceKm * 1000)} m`
|
|
: `${distanceKm.toFixed(1)} km`}
|
|
</Badge>
|
|
)}
|
|
{item.shipping && (
|
|
<Badge variant="outline" className="text-xs border-gray-600 bg-gray-800/50">
|
|
{item.shipping === 'buyer' ? t('marketUi.card.postageBuyer') : item.shipping === 'seller' ? t('marketUi.card.postageSeller') : t('marketUi.card.postageSplit')}
|
|
</Badge>
|
|
)}
|
|
{typeof item.powDifficulty === 'number' && item.powDifficulty >= 16 && (
|
|
<Badge
|
|
variant="outline"
|
|
className="text-xs border-gray-600 bg-gray-800/50"
|
|
title={t('marketUi.card.powTitle')}
|
|
>
|
|
⚡ PoW {item.powDifficulty}
|
|
</Badge>
|
|
)}
|
|
{sellerRep && sellerRep.verified > 0 && (
|
|
<Badge variant="outline" className="text-xs border-[#ccff00]/40 bg-[#ccff00]/10 text-[#ccff00]">
|
|
<CheckCircle className="h-3 w-3 mr-1" />
|
|
{sellerRep.verified === 1 ? t('marketUi.card.verifiedTradeOne', { count: sellerRep.verified }) : t('marketUi.card.verifiedTrades', { count: sellerRep.verified })}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
|
|
{/* Description */}
|
|
{(item.summary || item.description) && !compact && (
|
|
<p className="text-sm text-gray-300 line-clamp-2 mb-3">
|
|
{item.summary || item.description}
|
|
</p>
|
|
)}
|
|
|
|
{/* Seller Info with enhanced trust indicators */}
|
|
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
|
<Avatar className="h-6 w-6 border border-orange-500/30">
|
|
<AvatarImage src={avatar} alt={displayName} />
|
|
<AvatarFallback className="bg-orange-500/20 text-orange-400 text-xs">
|
|
{displayName.slice(0, 2).toUpperCase()}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="flex-1 min-w-0 text-xs text-gray-400">
|
|
<span className="font-medium text-gray-300">{displayName}</span>
|
|
<span className="mx-1">•</span>
|
|
<span>{timeAgo}</span>
|
|
</div>
|
|
|
|
{/* Trust indicators */}
|
|
<div className="flex items-center gap-1">
|
|
{isVerifiedSeller && (
|
|
<Badge variant="outline" className="text-xs border-green-500/50 text-green-400 bg-green-500/10 gap-1">
|
|
<CheckCircle className="h-3 w-3" />
|
|
{t('common.verified')}
|
|
</Badge>
|
|
)}
|
|
{isHighTrust && !isVerifiedSeller && (
|
|
<Badge variant="outline" className="text-xs border-blue-500/50 text-blue-400 bg-blue-500/10 gap-1">
|
|
<ShieldCheck className="h-3 w-3" />
|
|
{t('marketUi.card.trusted')}
|
|
</Badge>
|
|
)}
|
|
<TrustBadge pubkey={item.pubkey} size="sm" showLabel={false} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Seller stats (account age, listings) */}
|
|
{!compact && sellerInfo && (
|
|
<div className="flex items-center gap-3 text-xs text-gray-500 mb-2">
|
|
{accountAgeDays > 0 && (
|
|
<div className="flex items-center gap-1">
|
|
<Calendar className="h-3 w-3" />
|
|
<span>
|
|
{accountAgeDays < 30
|
|
? t('marketUi.card.daysOld', { n: accountAgeDays })
|
|
: accountAgeDays < 365
|
|
? t('marketUi.card.monthsOld', { n: Math.floor(accountAgeDays / 30) })
|
|
: t('marketUi.card.yearsOld', { n: Math.floor(accountAgeDays / 365) })}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{previousListings > 1 && (
|
|
<div className="flex items-center gap-1">
|
|
<Package className="h-3 w-3" />
|
|
<span>{t('marketUi.card.listingsCount', { count: previousListings })}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Availability */}
|
|
{item.availability && (
|
|
<div className="flex items-center gap-1 text-xs text-gray-400 mb-3">
|
|
<Clock className="h-3 w-3 text-orange-500" />
|
|
<span>{t('marketUi.listingDetail.usuallyHere', { availability: item.availability })}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Status Badge */}
|
|
{item.status && item.status !== 'active' && (
|
|
<Badge
|
|
variant="secondary"
|
|
className={`text-xs mb-2 ${
|
|
item.status === 'sold' ? 'bg-gray-700 text-gray-400' : 'bg-yellow-900/50 text-yellow-400'
|
|
}`}
|
|
>
|
|
{item.status.toUpperCase()}
|
|
</Badge>
|
|
)}
|
|
|
|
{/* Action Buttons */}
|
|
<ListingTradeActions item={item} />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
|
|
{/* Seller: close the deal (mark sold + publish trade attestation) */}
|
|
{isOwnListing && (
|
|
<MarkSoldDialog
|
|
open={showMarkSold}
|
|
onOpenChange={setShowMarkSold}
|
|
event={item.event}
|
|
listing={listingRef}
|
|
/>
|
|
)}
|
|
|
|
|
|
{/* Delete confirmation (controlled — trigger lives in the owner dropdown) */}
|
|
<DeleteEntityDialog
|
|
event={item.event}
|
|
entityLabel="listing"
|
|
entityName={item.title}
|
|
triggerText={t('marketUi.listingDetail.deleteListing')}
|
|
open={showDeleteDialog}
|
|
onOpenChange={setShowDeleteDialog}
|
|
/>
|
|
|
|
{/* Report Dialog */}
|
|
<Dialog open={showReportDialog} onOpenChange={setShowReportDialog}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<Flag className="h-5 w-5 text-red-400" />
|
|
{t('marketUi.card.reportListing')}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{t('marketUi.card.reportDialogBody')}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label>{t('marketUi.card.reasonLabel')}</Label>
|
|
<div className="grid grid-cols-1 gap-2">
|
|
{Object.entries(LISTING_REPORT_REASONS).map(([key, { label, description }]) => (
|
|
<Button
|
|
key={key}
|
|
type="button"
|
|
variant={reportReason === key ? 'default' : 'outline'}
|
|
className={`h-auto py-2 px-3 justify-start text-left ${
|
|
reportReason === key
|
|
? 'bg-red-500 hover:bg-red-600'
|
|
: 'border-gray-600 hover:bg-gray-800'
|
|
}`}
|
|
onClick={() => setReportReason(key)}
|
|
>
|
|
<div>
|
|
<div className="font-medium">{label}</div>
|
|
<div className="text-xs opacity-70">{description}</div>
|
|
</div>
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="report-details">{t('marketUi.card.detailsLabel')}</Label>
|
|
<Textarea
|
|
id="report-details"
|
|
placeholder={t('marketUi.card.detailsPlaceholder')}
|
|
value={reportDetails}
|
|
onChange={(e) => setReportDetails(e.target.value)}
|
|
rows={3}
|
|
className="bg-gray-800 border-gray-700"
|
|
/>
|
|
</div>
|
|
|
|
<Card className="bg-gray-800/50 border-gray-700">
|
|
<CardContent className="p-3">
|
|
<p className="text-xs text-gray-400">
|
|
{t('marketUi.card.reportNote')}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setShowReportDialog(false)}>
|
|
{t('common.cancel')}
|
|
</Button>
|
|
<Button
|
|
onClick={handleReport}
|
|
disabled={isReporting || !reportReason}
|
|
className="bg-red-500 hover:bg-red-600"
|
|
>
|
|
{isReporting ? t('marketUi.card.submitting') : t('marketUi.card.submitReport')}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</Card>
|
|
);
|
|
}
|