style: clean up code formatting and remove trailing whitespace
refactor: reorganize imports and fix type imports
This commit is contained in:
1 parent
d3704602fd
commit
47c04de6fe
232 files changed
+7140
-4798
No files matched your search
+485
-278
File diff suppressed because it is too large.
Load diff
@@ -1,10 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Clock,
|
||||
Eye,
|
||||
Heart,
|
||||
MessageSquare,
|
||||
MousePointerClick,
|
||||
TrendingUp,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useSession } from "@/lib/hooks/use-session";
|
||||
import { TrendingUp, Eye, MousePointerClick, Heart, MessageSquare, Clock } from "lucide-react";
|
||||
|
||||
interface AdAnalytics {
|
||||
ad_id: number;
|
||||
@@ -37,13 +44,13 @@ export function AnalyticsDashboard() {
|
||||
if (user?.userId) {
|
||||
loadUserListings();
|
||||
}
|
||||
}, [user?.userId]);
|
||||
}, [user?.userId, loadUserListings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedAd) {
|
||||
loadAnalytics(selectedAd);
|
||||
}
|
||||
}, [selectedAd]);
|
||||
}, [selectedAd, loadAnalytics]);
|
||||
|
||||
const loadUserListings = async () => {
|
||||
setLoading(true);
|
||||
@@ -266,4 +273,3 @@ export function AnalyticsDashboard() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Clock } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AuctionCountdownProps {
|
||||
@@ -30,7 +30,7 @@ export function AuctionCountdown({
|
||||
|
||||
const calculateTimeRemaining = () => {
|
||||
const endTime = new Date(endsAt).getTime();
|
||||
const now = new Date().getTime();
|
||||
const now = Date.now();
|
||||
const total = endTime - now;
|
||||
|
||||
if (total <= 0) {
|
||||
@@ -61,19 +61,19 @@ export function AuctionCountdown({
|
||||
// Determinar cor baseada no tempo restante
|
||||
const getColorClass = () => {
|
||||
const totalMinutes = timeRemaining.hours * 60 + timeRemaining.minutes;
|
||||
|
||||
|
||||
if (timeRemaining.total <= 0) {
|
||||
return "text-red-600 dark:text-red-400";
|
||||
}
|
||||
|
||||
|
||||
if (totalMinutes < 5) {
|
||||
return "text-red-500 dark:text-red-400 font-semibold animate-pulse";
|
||||
}
|
||||
|
||||
|
||||
if (totalMinutes < 60) {
|
||||
return "text-yellow-600 dark:text-yellow-400";
|
||||
}
|
||||
|
||||
|
||||
return "text-green-600 dark:text-green-400";
|
||||
};
|
||||
|
||||
@@ -84,7 +84,7 @@ export function AuctionCountdown({
|
||||
}
|
||||
|
||||
const totalMinutes = timeRemaining.hours * 60 + timeRemaining.minutes;
|
||||
|
||||
|
||||
if (totalMinutes < 5) {
|
||||
return "Termina em breve";
|
||||
}
|
||||
@@ -102,13 +102,10 @@ export function AuctionCountdown({
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1.5", className)}>
|
||||
{showIcon && (
|
||||
<Clock className={cn("h-3.5 w-3.5", getColorClass())} />
|
||||
)}
|
||||
{showIcon && <Clock className={cn("h-3.5 w-3.5", getColorClass())} />}
|
||||
<span className={cn("text-xs font-medium", getColorClass())}>
|
||||
{getTimeText()}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Loader2, TrendingUp } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -10,7 +11,6 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Coins, DollarSign, Loader2, TrendingUp } from "lucide-react";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useSession } from "@/lib/hooks/use-session";
|
||||
|
||||
interface Listing {
|
||||
@@ -45,20 +45,27 @@ interface BidModalProps {
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
export function BidModal({ open, onOpenChange, listing, onSuccess }: BidModalProps) {
|
||||
export function BidModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
listing,
|
||||
onSuccess,
|
||||
}: BidModalProps) {
|
||||
const { user } = useSession();
|
||||
const [bidAmount, setBidAmount] = useState<string>("");
|
||||
const [paymentMethod, setPaymentMethod] = useState<string>("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [bidHistory, setBidHistory] = useState<Array<{
|
||||
bid_id: number;
|
||||
bidder_id: number;
|
||||
bidder_name: string;
|
||||
bid_amount: number;
|
||||
bid_type: string | null;
|
||||
created_at: Date;
|
||||
}>>([]);
|
||||
const [bidHistory, setBidHistory] = useState<
|
||||
Array<{
|
||||
bid_id: number;
|
||||
bidder_id: number;
|
||||
bidder_name: string;
|
||||
bid_amount: number;
|
||||
bid_type: string | null;
|
||||
created_at: Date;
|
||||
}>
|
||||
>([]);
|
||||
const [loadingHistory, setLoadingHistory] = useState(false);
|
||||
|
||||
if (!listing) return null;
|
||||
@@ -67,41 +74,50 @@ export function BidModal({ open, onOpenChange, listing, onSuccess }: BidModalPro
|
||||
const currentBid = listing.current_bid_amount || 0;
|
||||
const minIncrement = listing.min_bid_increment || 10;
|
||||
const minBid = currentBid + minIncrement;
|
||||
const initialPrice = listing.initial_price_pix || listing.initial_price_tibia_coins || listing.initial_price_gold_coins || 0;
|
||||
const initialPrice =
|
||||
listing.initial_price_pix ||
|
||||
listing.initial_price_tibia_coins ||
|
||||
listing.initial_price_gold_coins ||
|
||||
0;
|
||||
|
||||
// Determinar métodos de pagamento disponíveis para lance
|
||||
const availableMethods: string[] = [];
|
||||
if (listing.price_pix || listing.initial_price_pix) availableMethods.push("pix");
|
||||
if (listing.price_tibia_coins || listing.initial_price_tibia_coins) availableMethods.push("tc");
|
||||
if (listing.price_gold_coins || listing.initial_price_gold_coins) availableMethods.push("gp");
|
||||
if (listing.price_pix || listing.initial_price_pix)
|
||||
availableMethods.push("pix");
|
||||
if (listing.price_tibia_coins || listing.initial_price_tibia_coins)
|
||||
availableMethods.push("tc");
|
||||
if (listing.price_gold_coins || listing.initial_price_gold_coins)
|
||||
availableMethods.push("gp");
|
||||
|
||||
// Selecionar primeiro método disponível por padrão
|
||||
useEffect(() => {
|
||||
if (availableMethods.length > 0 && !paymentMethod) {
|
||||
setPaymentMethod(availableMethods[0]);
|
||||
}
|
||||
}, [availableMethods.length]);
|
||||
}, [paymentMethod]);
|
||||
|
||||
// Atualizar valor do lance quando mudar
|
||||
useEffect(() => {
|
||||
if (bidAmount === "" && minBid > 0) {
|
||||
setBidAmount(minBid.toFixed(2));
|
||||
}
|
||||
}, [minBid]);
|
||||
}, [minBid, bidAmount]);
|
||||
|
||||
// Buscar histórico de lances quando abrir modal
|
||||
useEffect(() => {
|
||||
if (open && listing) {
|
||||
fetchBidHistory();
|
||||
}
|
||||
}, [open, listing?.listing_id]);
|
||||
}, [open, listing?.listing_id, fetchBidHistory, listing]);
|
||||
|
||||
const fetchBidHistory = async () => {
|
||||
if (!listing) return;
|
||||
|
||||
setLoadingHistory(true);
|
||||
try {
|
||||
const res = await fetch(`/api/listings/${listing.listing_id}/bids?limit=5`);
|
||||
const res = await fetch(
|
||||
`/api/listings/${listing.listing_id}/bids?limit=5`,
|
||||
);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
@@ -117,14 +133,16 @@ export function BidModal({ open, onOpenChange, listing, onSuccess }: BidModalPro
|
||||
|
||||
const handleBid = async () => {
|
||||
const amount = parseFloat(bidAmount);
|
||||
|
||||
if (isNaN(amount) || amount <= 0) {
|
||||
|
||||
if (Number.isNaN(amount) || amount <= 0) {
|
||||
setError("Valor do lance inválido");
|
||||
return;
|
||||
}
|
||||
|
||||
if (amount < minBid) {
|
||||
setError(`Lance mínimo é ${minBid.toFixed(2)} (lance atual: ${currentBid.toFixed(2)} + incremento: ${minIncrement.toFixed(2)})`);
|
||||
setError(
|
||||
`Lance mínimo é ${minBid.toFixed(2)} (lance atual: ${currentBid.toFixed(2)} + incremento: ${minIncrement.toFixed(2)})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -198,11 +216,17 @@ export function BidModal({ open, onOpenChange, listing, onSuccess }: BidModalPro
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">Preço Inicial</Label>
|
||||
<p className="text-lg font-semibold">{formatPrice(initialPrice)}</p>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Preço Inicial
|
||||
</Label>
|
||||
<p className="text-lg font-semibold">
|
||||
{formatPrice(initialPrice)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">Lance Atual</Label>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Lance Atual
|
||||
</Label>
|
||||
<p className="text-lg font-semibold text-primary">
|
||||
{currentBid > 0 ? formatPrice(currentBid) : "Sem lances"}
|
||||
</p>
|
||||
@@ -211,18 +235,28 @@ export function BidModal({ open, onOpenChange, listing, onSuccess }: BidModalPro
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">Incremento Mínimo</Label>
|
||||
<p className="text-sm font-medium">{formatPrice(minIncrement)}</p>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Incremento Mínimo
|
||||
</Label>
|
||||
<p className="text-sm font-medium">
|
||||
{formatPrice(minIncrement)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">Lance Mínimo Necessário</Label>
|
||||
<p className="text-sm font-semibold text-green-600">{formatPrice(minBid)}</p>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Lance Mínimo Necessário
|
||||
</Label>
|
||||
<p className="text-sm font-semibold text-green-600">
|
||||
{formatPrice(minBid)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{listing.auction_ends_at && (
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">Término do Leilão</Label>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Término do Leilão
|
||||
</Label>
|
||||
<p className="text-sm">
|
||||
{new Date(listing.auction_ends_at).toLocaleString("pt-BR")}
|
||||
</p>
|
||||
@@ -235,7 +269,9 @@ export function BidModal({ open, onOpenChange, listing, onSuccess }: BidModalPro
|
||||
{/* Histórico de lances */}
|
||||
{bidHistory.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Histórico de Lances (Últimos 5)</Label>
|
||||
<Label className="text-sm font-medium">
|
||||
Histórico de Lances (Últimos 5)
|
||||
</Label>
|
||||
<div className="bg-muted rounded-lg p-3 space-y-2 max-h-48 overflow-y-auto">
|
||||
{bidHistory.map((bid, index) => {
|
||||
const isCurrentUser = user?.userId === bid.bidder_id;
|
||||
@@ -244,7 +280,9 @@ export function BidModal({ open, onOpenChange, listing, onSuccess }: BidModalPro
|
||||
<div
|
||||
key={bid.bid_id}
|
||||
className={`p-2 rounded ${
|
||||
isCurrentUser ? "bg-primary/10 border border-primary/20" : ""
|
||||
isCurrentUser
|
||||
? "bg-primary/10 border border-primary/20"
|
||||
: ""
|
||||
} ${isHighest ? "border-l-2 border-l-primary" : ""}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -343,7 +381,12 @@ export function BidModal({ open, onOpenChange, listing, onSuccess }: BidModalPro
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleBid}
|
||||
disabled={loading || !bidAmount || parseFloat(bidAmount) < minBid || !paymentMethod}
|
||||
disabled={
|
||||
loading ||
|
||||
!bidAmount ||
|
||||
parseFloat(bidAmount) < minBid ||
|
||||
!paymentMethod
|
||||
}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
@@ -363,4 +406,3 @@ export function BidModal({ open, onOpenChange, listing, onSuccess }: BidModalPro
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Package } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -8,12 +11,8 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -21,9 +20,10 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useSession } from "@/lib/hooks/use-session";
|
||||
import { ItemSearch } from "./ItemSearch";
|
||||
import { ArrowRight, Package } from "lucide-react";
|
||||
|
||||
interface Item {
|
||||
id: number;
|
||||
@@ -47,7 +47,11 @@ interface Server {
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export function CreateAdModal({ open, onOpenChange, onSuccess }: CreateAdModalProps) {
|
||||
export function CreateAdModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSuccess,
|
||||
}: CreateAdModalProps) {
|
||||
const { user } = useSession();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -97,7 +101,7 @@ export function CreateAdModal({ open, onOpenChange, onSuccess }: CreateAdModalPr
|
||||
|
||||
if (!hasPrice && (!acceptTrade || !tradeItem)) {
|
||||
setError(
|
||||
"Informe pelo menos um preço (PIX, TC ou GP) ou configure uma troca por item"
|
||||
"Informe pelo menos um preço (PIX, TC ou GP) ou configure uma troca por item",
|
||||
);
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -130,9 +134,10 @@ export function CreateAdModal({ open, onOpenChange, onSuccess }: CreateAdModalPr
|
||||
description: formData.description || null,
|
||||
trade_item_id: acceptTrade && tradeItem ? tradeItem.id : null,
|
||||
trade_item_name: acceptTrade && tradeItem ? tradeItem.name : null,
|
||||
trade_item_quantity: acceptTrade && tradeItem
|
||||
? parseInt(formData.trade_item_quantity, 10)
|
||||
: null,
|
||||
trade_item_quantity:
|
||||
acceptTrade && tradeItem
|
||||
? parseInt(formData.trade_item_quantity, 10)
|
||||
: null,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -155,10 +160,14 @@ export function CreateAdModal({ open, onOpenChange, onSuccess }: CreateAdModalPr
|
||||
auction_ends_at: "Término do leilão",
|
||||
};
|
||||
|
||||
const validationMessages = data.details.map((detail: { field?: string; message?: string }) => {
|
||||
const fieldLabel = detail.field ? fieldLabels[detail.field] || detail.field : "Campo";
|
||||
return `${fieldLabel}: ${detail.message ?? "preenchimento obrigatório"}`;
|
||||
});
|
||||
const validationMessages = data.details.map(
|
||||
(detail: { field?: string; message?: string }) => {
|
||||
const fieldLabel = detail.field
|
||||
? fieldLabels[detail.field] || detail.field
|
||||
: "Campo";
|
||||
return `${fieldLabel}: ${detail.message ?? "preenchimento obrigatório"}`;
|
||||
},
|
||||
);
|
||||
|
||||
setError(validationMessages.join(" "));
|
||||
} else {
|
||||
@@ -199,7 +208,7 @@ export function CreateAdModal({ open, onOpenChange, onSuccess }: CreateAdModalPr
|
||||
if (open) {
|
||||
loadServers();
|
||||
}
|
||||
}, [open]);
|
||||
}, [open, loadServers]);
|
||||
|
||||
const loadServers = async () => {
|
||||
setLoadingServers(true);
|
||||
@@ -295,7 +304,10 @@ export function CreateAdModal({ open, onOpenChange, onSuccess }: CreateAdModalPr
|
||||
</SelectItem>
|
||||
) : (
|
||||
servers.map((server) => (
|
||||
<SelectItem key={server.serverId} value={server.serverName}>
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
value={server.serverName}
|
||||
>
|
||||
{server.serverName}
|
||||
{server.location && ` (${server.location})`}
|
||||
</SelectItem>
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Loader2,
|
||||
MessageSquare,
|
||||
ShoppingBag,
|
||||
ShoppingCart,
|
||||
User,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -8,24 +22,10 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import {
|
||||
ShoppingCart,
|
||||
Clock,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
MessageSquare,
|
||||
User,
|
||||
Loader2,
|
||||
ShoppingBag,
|
||||
} from "lucide-react";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useSession } from "@/lib/hooks/use-session";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
interface PurchaseIntention {
|
||||
intention_id: number;
|
||||
@@ -72,21 +72,30 @@ function getStatusBadge(status: string) {
|
||||
switch (status) {
|
||||
case "pending":
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-yellow-500/20 text-yellow-700 dark:text-yellow-400">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-yellow-500/20 text-yellow-700 dark:text-yellow-400"
|
||||
>
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
Pendente
|
||||
</Badge>
|
||||
);
|
||||
case "accepted":
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-green-500/20 text-green-700 dark:text-green-400">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-green-500/20 text-green-700 dark:text-green-400"
|
||||
>
|
||||
<CheckCircle2 className="h-3 w-3 mr-1" />
|
||||
Aceita
|
||||
</Badge>
|
||||
);
|
||||
case "rejected":
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-red-500/20 text-red-700 dark:text-red-400">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-red-500/20 text-red-700 dark:text-red-400"
|
||||
>
|
||||
<XCircle className="h-3 w-3 mr-1" />
|
||||
Rejeitada
|
||||
</Badge>
|
||||
@@ -96,7 +105,9 @@ function getStatusBadge(status: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function formatCustomOffer(intention: PurchaseIntention): { value: string; date: string | null } | null {
|
||||
function formatCustomOffer(
|
||||
intention: PurchaseIntention,
|
||||
): { value: string; date: string | null } | null {
|
||||
if (
|
||||
intention.custom_offer_amount === null ||
|
||||
intention.custom_offer_amount === undefined ||
|
||||
@@ -142,7 +153,10 @@ export function IntentionDetailModal({
|
||||
const [sellerResponse, setSellerResponse] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [listing, setListing] = useState<{ seller_id: number; listing_status: string } | null>(null);
|
||||
const [listing, setListing] = useState<{
|
||||
seller_id: number;
|
||||
listing_status: string;
|
||||
} | null>(null);
|
||||
const [showMarkSoldDialog, setShowMarkSoldDialog] = useState(false);
|
||||
const [markingSold, setMarkingSold] = useState(false);
|
||||
|
||||
@@ -161,7 +175,11 @@ export function IntentionDetailModal({
|
||||
}, [open, listingId]);
|
||||
|
||||
const isOwner = user && listing && user.userId === listing.seller_id;
|
||||
const canMarkAsSold = isOwner && listing && (listing.listing_status === "active" || listing.listing_status === "in_escrow");
|
||||
const canMarkAsSold =
|
||||
isOwner &&
|
||||
listing &&
|
||||
(listing.listing_status === "active" ||
|
||||
listing.listing_status === "in_escrow");
|
||||
const canAcceptOrReject = intention.status === "pending";
|
||||
const customOffer = formatCustomOffer(intention);
|
||||
|
||||
@@ -169,7 +187,7 @@ export function IntentionDetailModal({
|
||||
if (open) {
|
||||
setSellerResponse(intention.seller_response ?? "");
|
||||
}
|
||||
}, [open, intention.intention_id, intention.seller_response]);
|
||||
}, [open, intention.seller_response]);
|
||||
|
||||
const handleAccept = async () => {
|
||||
setLoading(true);
|
||||
@@ -185,7 +203,7 @@ export function IntentionDetailModal({
|
||||
status: "accepted",
|
||||
seller_response: sellerResponse || undefined,
|
||||
}),
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const data = await res.json();
|
||||
@@ -217,7 +235,7 @@ export function IntentionDetailModal({
|
||||
status: "rejected",
|
||||
seller_response: sellerResponse || undefined,
|
||||
}),
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const data = await res.json();
|
||||
@@ -229,7 +247,9 @@ export function IntentionDetailModal({
|
||||
if (onUpdate) onUpdate();
|
||||
onOpenChange(false);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Erro ao rejeitar intenção");
|
||||
setError(
|
||||
err instanceof Error ? err.message : "Erro ao rejeitar intenção",
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -241,7 +261,7 @@ export function IntentionDetailModal({
|
||||
const res = await fetch(`/api/listings/${listingId}/mark-sold`, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || "Erro ao marcar como vendido");
|
||||
@@ -250,13 +270,15 @@ export function IntentionDetailModal({
|
||||
// Invalidar cache para atualizar a lista
|
||||
queryClient.invalidateQueries({ queryKey: ["listings"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["my-listings"] });
|
||||
|
||||
|
||||
if (onUpdate) onUpdate();
|
||||
setShowMarkSoldDialog(false);
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
console.error("Erro ao marcar como vendido:", error);
|
||||
alert(error instanceof Error ? error.message : "Erro ao marcar como vendido");
|
||||
alert(
|
||||
error instanceof Error ? error.message : "Erro ao marcar como vendido",
|
||||
);
|
||||
} finally {
|
||||
setMarkingSold(false);
|
||||
}
|
||||
@@ -271,7 +293,8 @@ export function IntentionDetailModal({
|
||||
{getStatusBadge(intention.status)}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Detalhes da intenção de compra de {intention.buyer?.username || "usuário"}
|
||||
Detalhes da intenção de compra de{" "}
|
||||
{intention.buyer?.username || "usuário"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -304,7 +327,9 @@ export function IntentionDetailModal({
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">Método de Pagamento</Label>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Método de Pagamento
|
||||
</Label>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<ShoppingCart className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="font-medium">
|
||||
@@ -313,7 +338,9 @@ export function IntentionDetailModal({
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">Data de Criação</Label>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Data de Criação
|
||||
</Label>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm">
|
||||
@@ -328,9 +355,13 @@ export function IntentionDetailModal({
|
||||
<Label className="text-xs text-primary uppercase tracking-wide">
|
||||
Oferta do Comprador
|
||||
</Label>
|
||||
<div className="text-base font-semibold text-primary">{customOffer.value}</div>
|
||||
<div className="text-base font-semibold text-primary">
|
||||
{customOffer.value}
|
||||
</div>
|
||||
{customOffer.date && (
|
||||
<p className="text-[11px] text-primary/70">Enviada em {customOffer.date}</p>
|
||||
<p className="text-[11px] text-primary/70">
|
||||
Enviada em {customOffer.date}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -398,7 +429,11 @@ export function IntentionDetailModal({
|
||||
Marcar como Vendido
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={loading || markingSold}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={loading || markingSold}
|
||||
>
|
||||
Fechar
|
||||
</Button>
|
||||
{canAcceptOrReject && (
|
||||
@@ -420,7 +455,10 @@ export function IntentionDetailModal({
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button onClick={handleAccept} disabled={loading || markingSold}>
|
||||
<Button
|
||||
onClick={handleAccept}
|
||||
disabled={loading || markingSold}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
@@ -445,14 +483,18 @@ export function IntentionDetailModal({
|
||||
<DialogHeader>
|
||||
<DialogTitle>Marcar anúncio como vendido?</DialogTitle>
|
||||
<DialogDescription>
|
||||
Ao marcar este anúncio como vendido, as seguintes ações serão executadas:
|
||||
Ao marcar este anúncio como vendido, as seguintes ações serão
|
||||
executadas:
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2 pt-2">
|
||||
<ul className="list-disc list-inside space-y-1 text-sm text-muted-foreground">
|
||||
<li>O anúncio será marcado como "Vendido"</li>
|
||||
<li>O anúncio continuará visível na lista por mais 1 hora</li>
|
||||
<li>Após 1 hora, o anúncio sairá automaticamente da lista de anúncios ativos</li>
|
||||
<li>
|
||||
Após 1 hora, o anúncio sairá automaticamente da lista de
|
||||
anúncios ativos
|
||||
</li>
|
||||
<li>Nenhuma interação (comprar, dar lance) será permitida</li>
|
||||
</ul>
|
||||
<p className="pt-2 font-medium text-foreground">
|
||||
@@ -480,4 +522,3 @@ export function IntentionDetailModal({
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Loader2, Search, X } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Search, X, Loader2 } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Item {
|
||||
@@ -38,7 +38,9 @@ export function ItemSearch({
|
||||
const [items, setItems] = useState<Item[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const [previewItem, setPreviewItem] = useState<Item | null>(selectedItem || null);
|
||||
const [previewItem, setPreviewItem] = useState<Item | null>(
|
||||
selectedItem || null,
|
||||
);
|
||||
const searchRef = useRef<HTMLDivElement>(null);
|
||||
const debounceRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||
|
||||
@@ -58,7 +60,7 @@ export function ItemSearch({
|
||||
debounceRef.current = setTimeout(async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/items?q=${encodeURIComponent(searchQuery)}&pageSize=10`
|
||||
`/api/items?q=${encodeURIComponent(searchQuery)}&pageSize=10`,
|
||||
);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
@@ -76,7 +78,10 @@ export function ItemSearch({
|
||||
// Fechar dropdown ao clicar fora
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (searchRef.current && !searchRef.current.contains(event.target as Node)) {
|
||||
if (
|
||||
searchRef.current &&
|
||||
!searchRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setShowDropdown(false);
|
||||
}
|
||||
};
|
||||
@@ -159,7 +164,9 @@ export function ItemSearch({
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate">{item.name}</p>
|
||||
{item.category && (
|
||||
<p className="text-xs text-muted-foreground">{item.category}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{item.category}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{item.rarity && (
|
||||
@@ -200,24 +207,27 @@ export function ItemSearch({
|
||||
{previewItem.rarity && (
|
||||
<Badge variant="secondary">{previewItem.rarity}</Badge>
|
||||
)}
|
||||
{previewItem.level !== undefined && previewItem.level !== null && (
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="font-medium">Level:</span>
|
||||
{previewItem.level}
|
||||
</span>
|
||||
)}
|
||||
{previewItem.weight !== undefined && previewItem.weight !== null && (
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="font-medium">Peso:</span>
|
||||
{previewItem.weight} oz
|
||||
</span>
|
||||
)}
|
||||
{previewItem.npc_price !== undefined && previewItem.npc_price !== null && (
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="font-medium">Preço NPC:</span>
|
||||
{previewItem.npc_price.toLocaleString()} gp
|
||||
</span>
|
||||
)}
|
||||
{previewItem.level !== undefined &&
|
||||
previewItem.level !== null && (
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="font-medium">Level:</span>
|
||||
{previewItem.level}
|
||||
</span>
|
||||
)}
|
||||
{previewItem.weight !== undefined &&
|
||||
previewItem.weight !== null && (
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="font-medium">Peso:</span>
|
||||
{previewItem.weight} oz
|
||||
</span>
|
||||
)}
|
||||
{previewItem.npc_price !== undefined &&
|
||||
previewItem.npc_price !== null && (
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="font-medium">Preço NPC:</span>
|
||||
{previewItem.npc_price.toLocaleString()} gp
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -227,4 +237,3 @@ export function ItemSearch({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,34 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Eye,
|
||||
Heart,
|
||||
MapPin,
|
||||
Pause,
|
||||
ShoppingBag,
|
||||
Star,
|
||||
Trash2,
|
||||
TrendingUp,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Eye,
|
||||
Heart,
|
||||
Clock,
|
||||
MapPin,
|
||||
DollarSign,
|
||||
Coins,
|
||||
TrendingUp,
|
||||
Star,
|
||||
Pause,
|
||||
Trash2,
|
||||
X,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
ShoppingBag,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useSession } from "@/lib/hooks/use-session";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
interface Listing {
|
||||
listing_id: number;
|
||||
@@ -76,10 +74,11 @@ interface ListingDetailsModalProps {
|
||||
|
||||
function formatTimeAgo(date: Date | string): string {
|
||||
const dateObj = typeof date === "string" ? new Date(date) : date;
|
||||
const seconds = Math.floor((new Date().getTime() - dateObj.getTime()) / 1000);
|
||||
const seconds = Math.floor((Date.now() - dateObj.getTime()) / 1000);
|
||||
if (seconds < 60) return "há alguns segundos";
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) return `há ${minutes} ${minutes === 1 ? "minuto" : "minutos"}`;
|
||||
if (minutes < 60)
|
||||
return `há ${minutes} ${minutes === 1 ? "minuto" : "minutos"}`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `há ${hours} ${hours === 1 ? "hora" : "horas"}`;
|
||||
const days = Math.floor(hours / 24);
|
||||
@@ -92,14 +91,40 @@ function formatTimeAgo(date: Date | string): string {
|
||||
|
||||
function getStatusBadge(status: string) {
|
||||
const statusConfig = {
|
||||
active: { label: "Ativo", variant: "default" as const, icon: CheckCircle2, className: "bg-green-500 hover:bg-green-600" },
|
||||
sold: { label: "Vendido", variant: "secondary" as const, icon: CheckCircle2, className: "bg-blue-500 hover:bg-blue-600 text-white" },
|
||||
cancelled: { label: "Cancelado", variant: "destructive" as const, icon: X, className: "bg-red-500 hover:bg-red-600 text-white" },
|
||||
in_escrow: { label: "Em Escrow", variant: "default" as const, icon: AlertCircle, className: "bg-yellow-500 hover:bg-yellow-600 text-white" },
|
||||
expired: { label: "Expirado", variant: "secondary" as const, icon: Clock, className: "bg-gray-500 hover:bg-gray-600 text-white" },
|
||||
active: {
|
||||
label: "Ativo",
|
||||
variant: "default" as const,
|
||||
icon: CheckCircle2,
|
||||
className: "bg-green-500 hover:bg-green-600",
|
||||
},
|
||||
sold: {
|
||||
label: "Vendido",
|
||||
variant: "secondary" as const,
|
||||
icon: CheckCircle2,
|
||||
className: "bg-blue-500 hover:bg-blue-600 text-white",
|
||||
},
|
||||
cancelled: {
|
||||
label: "Cancelado",
|
||||
variant: "destructive" as const,
|
||||
icon: X,
|
||||
className: "bg-red-500 hover:bg-red-600 text-white",
|
||||
},
|
||||
in_escrow: {
|
||||
label: "Em Escrow",
|
||||
variant: "default" as const,
|
||||
icon: AlertCircle,
|
||||
className: "bg-yellow-500 hover:bg-yellow-600 text-white",
|
||||
},
|
||||
expired: {
|
||||
label: "Expirado",
|
||||
variant: "secondary" as const,
|
||||
icon: Clock,
|
||||
className: "bg-gray-500 hover:bg-gray-600 text-white",
|
||||
},
|
||||
};
|
||||
|
||||
const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.active;
|
||||
const config =
|
||||
statusConfig[status as keyof typeof statusConfig] || statusConfig.active;
|
||||
const Icon = config.icon;
|
||||
|
||||
return (
|
||||
@@ -123,7 +148,7 @@ export function ListingDetailsModal({
|
||||
const [itemImageUrl, setItemImageUrl] = useState<string>("");
|
||||
const [showMarkSoldDialog, setShowMarkSoldDialog] = useState(false);
|
||||
const [markingSold, setMarkingSold] = useState(false);
|
||||
|
||||
|
||||
// Verificar se o usuário é o dono do anúncio
|
||||
const isOwner = user && user.userId === listing.seller_id;
|
||||
|
||||
@@ -134,13 +159,19 @@ export function ListingDetailsModal({
|
||||
} else if (listing.image_local_path) {
|
||||
const normalizedPath = listing.image_local_path.replace(/\\/g, "/");
|
||||
if (normalizedPath.startsWith("data/")) {
|
||||
setItemImageUrl(`/api/images?path=${encodeURIComponent(normalizedPath)}`);
|
||||
setItemImageUrl(
|
||||
`/api/images?path=${encodeURIComponent(normalizedPath)}`,
|
||||
);
|
||||
} else {
|
||||
const category = listing.item_category || "other";
|
||||
setItemImageUrl(`/api/images?path=${encodeURIComponent(`data/images/${category}/${normalizedPath}`)}`);
|
||||
setItemImageUrl(
|
||||
`/api/images?path=${encodeURIComponent(`data/images/${category}/${normalizedPath}`)}`,
|
||||
);
|
||||
}
|
||||
} else if (listing.item_id_unified) {
|
||||
const params = new URLSearchParams({ item: listing.item_id_unified.toString() });
|
||||
const params = new URLSearchParams({
|
||||
item: listing.item_id_unified.toString(),
|
||||
});
|
||||
if (listing.item_name) {
|
||||
params.set("name", listing.item_name);
|
||||
}
|
||||
@@ -158,27 +189,52 @@ export function ListingDetailsModal({
|
||||
const prices: Array<{ type: string; label: string; value: string }> = [];
|
||||
|
||||
if (listing.price_pix) {
|
||||
const price = typeof listing.price_pix === "string" ? parseFloat(listing.price_pix) : listing.price_pix;
|
||||
if (!isNaN(price)) {
|
||||
prices.push({ type: "pix", label: "PIX", value: `R$ ${price.toFixed(2)}` });
|
||||
const price =
|
||||
typeof listing.price_pix === "string"
|
||||
? parseFloat(listing.price_pix)
|
||||
: listing.price_pix;
|
||||
if (!Number.isNaN(price)) {
|
||||
prices.push({
|
||||
type: "pix",
|
||||
label: "PIX",
|
||||
value: `R$ ${price.toFixed(2)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (listing.price_tibia_coins) {
|
||||
const price = typeof listing.price_tibia_coins === "string" ? parseInt(listing.price_tibia_coins, 10) : listing.price_tibia_coins;
|
||||
if (!isNaN(price)) {
|
||||
prices.push({ type: "tibia_coins", label: "Tibia Coins", value: `${price.toLocaleString()} TC` });
|
||||
const price =
|
||||
typeof listing.price_tibia_coins === "string"
|
||||
? parseInt(listing.price_tibia_coins, 10)
|
||||
: listing.price_tibia_coins;
|
||||
if (!Number.isNaN(price)) {
|
||||
prices.push({
|
||||
type: "tibia_coins",
|
||||
label: "Tibia Coins",
|
||||
value: `${price.toLocaleString()} TC`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (listing.price_gold_coins) {
|
||||
const price = typeof listing.price_gold_coins === "string" ? parseInt(listing.price_gold_coins, 10) : listing.price_gold_coins;
|
||||
if (!isNaN(price)) {
|
||||
prices.push({ type: "gold_coins", label: "Gold Coins", value: `${price.toLocaleString()} GP` });
|
||||
const price =
|
||||
typeof listing.price_gold_coins === "string"
|
||||
? parseInt(listing.price_gold_coins, 10)
|
||||
: listing.price_gold_coins;
|
||||
if (!Number.isNaN(price)) {
|
||||
prices.push({
|
||||
type: "gold_coins",
|
||||
label: "Gold Coins",
|
||||
value: `${price.toLocaleString()} GP`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (listing.trade_item_id && listing.trade_item_name && listing.trade_item_quantity) {
|
||||
if (
|
||||
listing.trade_item_id &&
|
||||
listing.trade_item_name &&
|
||||
listing.trade_item_quantity
|
||||
) {
|
||||
prices.push({
|
||||
type: "trade",
|
||||
label: "Troca",
|
||||
@@ -220,7 +276,10 @@ export function ListingDetailsModal({
|
||||
const canPause = isOwner && listing.listing_status === "active";
|
||||
const canActivate = isOwner && listing.listing_status === "cancelled";
|
||||
const canCancel = isOwner && listing.listing_status === "active";
|
||||
const canMarkAsSold = isOwner && (listing.listing_status === "active" || listing.listing_status === "in_escrow");
|
||||
const canMarkAsSold =
|
||||
isOwner &&
|
||||
(listing.listing_status === "active" ||
|
||||
listing.listing_status === "in_escrow");
|
||||
|
||||
const handleMarkAsSold = async () => {
|
||||
setMarkingSold(true);
|
||||
@@ -228,7 +287,7 @@ export function ListingDetailsModal({
|
||||
const res = await fetch(`/api/listings/${listing.listing_id}/mark-sold`, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || "Erro ao marcar como vendido");
|
||||
@@ -237,12 +296,14 @@ export function ListingDetailsModal({
|
||||
// Invalidar cache para atualizar a lista
|
||||
queryClient.invalidateQueries({ queryKey: ["listings"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["my-listings"] });
|
||||
|
||||
|
||||
setShowMarkSoldDialog(false);
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
console.error("Erro ao marcar como vendido:", error);
|
||||
alert(error instanceof Error ? error.message : "Erro ao marcar como vendido");
|
||||
alert(
|
||||
error instanceof Error ? error.message : "Erro ao marcar como vendido",
|
||||
);
|
||||
} finally {
|
||||
setMarkingSold(false);
|
||||
}
|
||||
@@ -256,7 +317,9 @@ export function ListingDetailsModal({
|
||||
Detalhes do Anúncio
|
||||
{getStatusBadge(listing.listing_status)}
|
||||
</DialogTitle>
|
||||
<DialogDescription>Informações completas do seu anúncio</DialogDescription>
|
||||
<DialogDescription>
|
||||
Informações completas do seu anúncio
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
@@ -294,22 +357,30 @@ export function ListingDetailsModal({
|
||||
<div className="flex items-center gap-2">
|
||||
<Eye className="h-4 w-4 text-muted-foreground" />
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">Visualizações</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Visualizações
|
||||
</div>
|
||||
<div className="font-semibold">{listing.views_count}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Heart className="h-4 w-4 text-muted-foreground" />
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">Favoritos</div>
|
||||
<div className="font-semibold">{listing.favorites_count || 0}</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Favoritos
|
||||
</div>
|
||||
<div className="font-semibold">
|
||||
{listing.favorites_count || 0}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">Criado</div>
|
||||
<div className="font-semibold text-xs">{formatTimeAgo(listing.created_at)}</div>
|
||||
<div className="font-semibold text-xs">
|
||||
{formatTimeAgo(listing.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -349,7 +420,9 @@ export function ListingDetailsModal({
|
||||
: "bg-purple-500/10 border-purple-500/20"
|
||||
}`}
|
||||
>
|
||||
<div className="text-xs text-muted-foreground mb-1">{price.label}</div>
|
||||
<div className="text-xs text-muted-foreground mb-1">
|
||||
{price.label}
|
||||
</div>
|
||||
<div className="font-semibold">{price.value}</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -370,26 +443,44 @@ export function ListingDetailsModal({
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Lance Atual:</span>
|
||||
<span className="font-semibold text-lg text-primary">
|
||||
R$ {typeof listing.current_bid_amount === "number" ? listing.current_bid_amount.toFixed(2) : parseFloat(String(listing.current_bid_amount)).toFixed(2)}
|
||||
R${" "}
|
||||
{typeof listing.current_bid_amount === "number"
|
||||
? listing.current_bid_amount.toFixed(2)
|
||||
: parseFloat(
|
||||
String(listing.current_bid_amount),
|
||||
).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
{listing.min_bid_increment && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Incremento Mínimo:</span>
|
||||
<span className="text-muted-foreground">
|
||||
Incremento Mínimo:
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
R$ {typeof listing.min_bid_increment === "number" ? listing.min_bid_increment.toFixed(2) : parseFloat(String(listing.min_bid_increment)).toFixed(2)}
|
||||
R${" "}
|
||||
{typeof listing.min_bid_increment === "number"
|
||||
? listing.min_bid_increment.toFixed(2)
|
||||
: parseFloat(
|
||||
String(listing.min_bid_increment),
|
||||
).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{listing.auction_ends_at && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Termina em:</span>
|
||||
<span className="font-medium">{new Date(listing.auction_ends_at).toLocaleString("pt-BR")}</span>
|
||||
<span className="font-medium">
|
||||
{new Date(listing.auction_ends_at).toLocaleString(
|
||||
"pt-BR",
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-muted-foreground py-2">Sem lances ainda</div>
|
||||
<div className="text-center text-muted-foreground py-2">
|
||||
Sem lances ainda
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -398,7 +489,9 @@ export function ListingDetailsModal({
|
||||
{listing.description && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-semibold">Descrição</h4>
|
||||
<p className="text-sm text-muted-foreground p-3 bg-muted rounded-lg">{listing.description}</p>
|
||||
<p className="text-sm text-muted-foreground p-3 bg-muted rounded-lg">
|
||||
{listing.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -410,13 +503,19 @@ export function ListingDetailsModal({
|
||||
</div>
|
||||
{listing.item_id_unified && (
|
||||
<div>
|
||||
<span className="text-muted-foreground">ID do Item (Unificado):</span>
|
||||
<span className="ml-2 font-mono">{listing.item_id_unified}</span>
|
||||
<span className="text-muted-foreground">
|
||||
ID do Item (Unificado):
|
||||
</span>
|
||||
<span className="ml-2 font-mono">
|
||||
{listing.item_id_unified}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<span className="text-muted-foreground">Criado em:</span>
|
||||
<span className="ml-2">{new Date(listing.created_at).toLocaleString("pt-BR")}</span>
|
||||
<span className="ml-2">
|
||||
{new Date(listing.created_at).toLocaleString("pt-BR")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -424,9 +523,9 @@ export function ListingDetailsModal({
|
||||
{(canPause || canActivate || canCancel || canMarkAsSold) && (
|
||||
<div className="flex gap-2 pt-4 border-t">
|
||||
{canMarkAsSold && (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => setShowMarkSoldDialog(true)}
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => setShowMarkSoldDialog(true)}
|
||||
className="flex-1 bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
<ShoppingBag className="h-4 w-4 mr-2" />
|
||||
@@ -440,13 +539,21 @@ export function ListingDetailsModal({
|
||||
</Button>
|
||||
)}
|
||||
{canActivate && onActivate && (
|
||||
<Button variant="default" onClick={onActivate} className="flex-1">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={onActivate}
|
||||
className="flex-1"
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4 mr-2" />
|
||||
Reativar Anúncio
|
||||
</Button>
|
||||
)}
|
||||
{canCancel && (
|
||||
<Button variant="destructive" onClick={onCancel} className="flex-1">
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={onCancel}
|
||||
className="flex-1"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Cancelar Anúncio
|
||||
</Button>
|
||||
@@ -462,14 +569,18 @@ export function ListingDetailsModal({
|
||||
<DialogHeader>
|
||||
<DialogTitle>Marcar anúncio como vendido?</DialogTitle>
|
||||
<DialogDescription>
|
||||
Ao marcar este anúncio como vendido, as seguintes ações serão executadas:
|
||||
Ao marcar este anúncio como vendido, as seguintes ações serão
|
||||
executadas:
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2 pt-2">
|
||||
<ul className="list-disc list-inside space-y-1 text-sm text-muted-foreground">
|
||||
<li>O anúncio será marcado como "Vendido"</li>
|
||||
<li>O anúncio continuará visível na lista por mais 1 hora</li>
|
||||
<li>Após 1 hora, o anúncio sairá automaticamente da lista de anúncios ativos</li>
|
||||
<li>
|
||||
Após 1 hora, o anúncio sairá automaticamente da lista de
|
||||
anúncios ativos
|
||||
</li>
|
||||
<li>Nenhuma interação (comprar, dar lance) será permitida</li>
|
||||
</ul>
|
||||
<p className="pt-2 font-medium text-foreground">
|
||||
@@ -497,4 +608,3 @@ export function ListingDetailsModal({
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
MessageSquare,
|
||||
ShoppingCart,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { ShoppingCart, Clock, CheckCircle2, XCircle, MessageSquare } from "lucide-react";
|
||||
import { IntentionDetailModal } from "./IntentionDetailModal";
|
||||
|
||||
interface PurchaseIntention {
|
||||
@@ -32,10 +38,11 @@ interface MyAdsIntentionListProps {
|
||||
}
|
||||
|
||||
function formatTimeAgo(date: Date): string {
|
||||
const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000);
|
||||
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
||||
if (seconds < 60) return "há alguns segundos";
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) return `há ${minutes} ${minutes === 1 ? "minuto" : "minutos"}`;
|
||||
if (minutes < 60)
|
||||
return `há ${minutes} ${minutes === 1 ? "minuto" : "minutos"}`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `há ${hours} ${hours === 1 ? "hora" : "horas"}`;
|
||||
const days = Math.floor(hours / 24);
|
||||
@@ -46,21 +53,30 @@ function getStatusBadge(status: string) {
|
||||
switch (status) {
|
||||
case "pending":
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-yellow-500/20 text-yellow-700 dark:text-yellow-400">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-yellow-500/20 text-yellow-700 dark:text-yellow-400"
|
||||
>
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
Pendente
|
||||
</Badge>
|
||||
);
|
||||
case "accepted":
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-green-500/20 text-green-700 dark:text-green-400">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-green-500/20 text-green-700 dark:text-green-400"
|
||||
>
|
||||
<CheckCircle2 className="h-3 w-3 mr-1" />
|
||||
Aceita
|
||||
</Badge>
|
||||
);
|
||||
case "rejected":
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-red-500/20 text-red-700 dark:text-red-400">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-red-500/20 text-red-700 dark:text-red-400"
|
||||
>
|
||||
<XCircle className="h-3 w-3 mr-1" />
|
||||
Rejeitada
|
||||
</Badge>
|
||||
@@ -107,15 +123,21 @@ function formatCustomOffer(intention: PurchaseIntention): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
export function MyAdsIntentionList({ listingId, sellerId }: MyAdsIntentionListProps) {
|
||||
export function MyAdsIntentionList({
|
||||
listingId,
|
||||
sellerId,
|
||||
}: MyAdsIntentionListProps) {
|
||||
const [intentions, setIntentions] = useState<PurchaseIntention[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedIntention, setSelectedIntention] = useState<PurchaseIntention | null>(null);
|
||||
const [filter, setFilter] = useState<"all" | "pending" | "accepted" | "rejected">("all");
|
||||
const [selectedIntention, setSelectedIntention] =
|
||||
useState<PurchaseIntention | null>(null);
|
||||
const [filter, setFilter] = useState<
|
||||
"all" | "pending" | "accepted" | "rejected"
|
||||
>("all");
|
||||
|
||||
useEffect(() => {
|
||||
loadIntentions();
|
||||
}, [listingId, filter]);
|
||||
}, [loadIntentions]);
|
||||
|
||||
const loadIntentions = async () => {
|
||||
setLoading(true);
|
||||
@@ -124,7 +146,9 @@ export function MyAdsIntentionList({ listingId, sellerId }: MyAdsIntentionListPr
|
||||
if (filter !== "all") {
|
||||
params.set("status", filter);
|
||||
}
|
||||
const res = await fetch(`/api/listings/${listingId}/intentions?${params.toString()}`);
|
||||
const res = await fetch(
|
||||
`/api/listings/${listingId}/intentions?${params.toString()}`,
|
||||
);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
@@ -216,19 +240,26 @@ export function MyAdsIntentionList({ listingId, sellerId }: MyAdsIntentionListPr
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground mb-2">
|
||||
<ShoppingCart className="h-3 w-3" />
|
||||
<span>{getPaymentMethodLabel(intention.payment_method)}</span>
|
||||
<span>
|
||||
{getPaymentMethodLabel(intention.payment_method)}
|
||||
</span>
|
||||
<span>•</span>
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>{formatTimeAgo(new Date(intention.created_at))}</span>
|
||||
</div>
|
||||
{formatCustomOffer(intention) && (
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Badge variant="outline" className="border-primary text-primary">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-primary text-primary"
|
||||
>
|
||||
Oferta: {formatCustomOffer(intention)}
|
||||
</Badge>
|
||||
{intention.custom_offer_created_at && (
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{new Date(intention.custom_offer_created_at).toLocaleString("pt-BR")}
|
||||
{new Date(
|
||||
intention.custom_offer_created_at,
|
||||
).toLocaleString("pt-BR")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -261,4 +292,3 @@ export function MyAdsIntentionList({ listingId, sellerId }: MyAdsIntentionListPr
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,39 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Pause,
|
||||
Play,
|
||||
Trash2,
|
||||
Edit,
|
||||
ChevronDown,
|
||||
Eye,
|
||||
Heart,
|
||||
TrendingUp,
|
||||
ChevronDown,
|
||||
Pause,
|
||||
Play,
|
||||
ShoppingCart,
|
||||
Trash2,
|
||||
TrendingUp,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useMyListings } from "@/lib/hooks/use-listings";
|
||||
import { useSession } from "@/lib/hooks/use-session";
|
||||
import { useMyListings, type Listing } from "@/lib/hooks/use-listings";
|
||||
import { AdCard } from "./AdCard";
|
||||
import { MyAdsIntentionList } from "./MyAdsIntentionList";
|
||||
|
||||
export function MyAdsPanel() {
|
||||
const { user } = useSession();
|
||||
const queryClient = useQueryClient();
|
||||
const [activeTab, setActiveTab] = useState<string>("active");
|
||||
const [expandedIntentions, setExpandedIntentions] = useState<Set<number>>(new Set());
|
||||
const [expandedIntentions, setExpandedIntentions] = useState<Set<number>>(
|
||||
new Set(),
|
||||
);
|
||||
|
||||
// Determinar status baseado na aba ativa
|
||||
const status =
|
||||
activeTab === "active" ? "active" :
|
||||
activeTab === "paused" ? "cancelled" : // Backend usa 'cancelled' para pausados
|
||||
activeTab === "sold" ? "sold" :
|
||||
undefined; // "all" busca todos
|
||||
const status =
|
||||
activeTab === "active"
|
||||
? "active"
|
||||
: activeTab === "paused"
|
||||
? "cancelled"
|
||||
: // Backend usa 'cancelled' para pausados
|
||||
activeTab === "sold"
|
||||
? "sold"
|
||||
: undefined; // "all" busca todos
|
||||
|
||||
// Usar React Query para buscar meus anúncios com atualização automática
|
||||
// O queryKey muda quando status muda, então recarrega automaticamente
|
||||
@@ -145,9 +149,7 @@ export function MyAdsPanel() {
|
||||
</div>
|
||||
) : listings.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-muted-foreground">
|
||||
Nenhum anúncio encontrado
|
||||
</p>
|
||||
<p className="text-muted-foreground">Nenhum anúncio encontrado</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
@@ -180,21 +182,25 @@ export function MyAdsPanel() {
|
||||
<span>
|
||||
{(() => {
|
||||
if (listing.price_pix) {
|
||||
const price = typeof listing.price_pix === 'string'
|
||||
? parseFloat(listing.price_pix)
|
||||
: listing.price_pix;
|
||||
const price =
|
||||
typeof listing.price_pix === "string"
|
||||
? parseFloat(listing.price_pix)
|
||||
: listing.price_pix;
|
||||
return `R$ ${price.toFixed(2)}`;
|
||||
}
|
||||
if (listing.price_tibia_coins) {
|
||||
const price = typeof listing.price_tibia_coins === 'string'
|
||||
? parseInt(listing.price_tibia_coins, 10)
|
||||
: listing.price_tibia_coins;
|
||||
const price =
|
||||
typeof listing.price_tibia_coins ===
|
||||
"string"
|
||||
? parseInt(listing.price_tibia_coins, 10)
|
||||
: listing.price_tibia_coins;
|
||||
return `${price.toLocaleString()} TC`;
|
||||
}
|
||||
if (listing.price_gold_coins) {
|
||||
const price = typeof listing.price_gold_coins === 'string'
|
||||
? parseInt(listing.price_gold_coins, 10)
|
||||
: listing.price_gold_coins;
|
||||
const price =
|
||||
typeof listing.price_gold_coins === "string"
|
||||
? parseInt(listing.price_gold_coins, 10)
|
||||
: listing.price_gold_coins;
|
||||
return `${price.toLocaleString()} GP`;
|
||||
}
|
||||
return "Preço não informado";
|
||||
@@ -236,42 +242,43 @@ export function MyAdsPanel() {
|
||||
</div>
|
||||
|
||||
{/* Seção de Intenções de Compra */}
|
||||
{listing.listing_status === "active" && !listing.is_auction && (
|
||||
<div className="border-t pt-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newExpanded = new Set(expandedIntentions);
|
||||
if (isExpanded) {
|
||||
newExpanded.delete(listing.listing_id);
|
||||
} else {
|
||||
newExpanded.add(listing.listing_id);
|
||||
}
|
||||
setExpandedIntentions(newExpanded);
|
||||
}}
|
||||
className="w-full justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<ShoppingCart className="h-4 w-4" />
|
||||
<span>Intenções de Compra</span>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 transition-transform ${
|
||||
isExpanded ? "transform rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
</Button>
|
||||
{isExpanded && (
|
||||
<div className="mt-4">
|
||||
<MyAdsIntentionList
|
||||
listingId={listing.listing_id}
|
||||
sellerId={listing.seller_id}
|
||||
{listing.listing_status === "active" &&
|
||||
!listing.is_auction && (
|
||||
<div className="border-t pt-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newExpanded = new Set(expandedIntentions);
|
||||
if (isExpanded) {
|
||||
newExpanded.delete(listing.listing_id);
|
||||
} else {
|
||||
newExpanded.add(listing.listing_id);
|
||||
}
|
||||
setExpandedIntentions(newExpanded);
|
||||
}}
|
||||
className="w-full justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<ShoppingCart className="h-4 w-4" />
|
||||
<span>Intenções de Compra</span>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 transition-transform ${
|
||||
isExpanded ? "transform rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
{isExpanded && (
|
||||
<div className="mt-4">
|
||||
<MyAdsIntentionList
|
||||
listingId={listing.listing_id}
|
||||
sellerId={listing.seller_id}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -282,4 +289,3 @@ export function MyAdsPanel() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,31 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import {
|
||||
Clock,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
MessageSquare,
|
||||
ShoppingCart,
|
||||
MapPin,
|
||||
Package,
|
||||
User,
|
||||
ExternalLink,
|
||||
AlertCircle,
|
||||
DollarSign,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Coins,
|
||||
DollarSign,
|
||||
ExternalLink,
|
||||
MapPin,
|
||||
MessageSquare,
|
||||
Package,
|
||||
RefreshCw,
|
||||
ShoppingCart,
|
||||
User,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import { useMyPurchaseIntentions, type PurchaseIntention, type PurchaseIntentionStatus } from "@/lib/hooks/use-purchase-intentions";
|
||||
import { useSession } from "@/lib/hooks/use-session";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { ChatModal } from "@/components/chat/ChatModal";
|
||||
import { ChangePaymentMethodButton } from "@/components/purchase-intentions/ChangePaymentMethodButton";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -33,14 +30,22 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { ChatModal } from "@/components/chat/ChatModal";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
type PurchaseIntention,
|
||||
type PurchaseIntentionStatus,
|
||||
useMyPurchaseIntentions,
|
||||
} from "@/lib/hooks/use-purchase-intentions";
|
||||
import { useSession } from "@/lib/hooks/use-session";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function formatTimeAgo(date: Date | string): string {
|
||||
const dateObj = typeof date === "string" ? new Date(date) : date;
|
||||
const seconds = Math.floor((new Date().getTime() - dateObj.getTime()) / 1000);
|
||||
const seconds = Math.floor((Date.now() - dateObj.getTime()) / 1000);
|
||||
if (seconds < 60) return "há alguns segundos";
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) return `há ${minutes} ${minutes === 1 ? "minuto" : "minutos"}`;
|
||||
if (minutes < 60)
|
||||
return `há ${minutes} ${minutes === 1 ? "minuto" : "minutos"}`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `há ${hours} ${hours === 1 ? "hora" : "horas"}`;
|
||||
const days = Math.floor(hours / 24);
|
||||
@@ -55,35 +60,50 @@ function getStatusBadge(status: PurchaseIntentionStatus) {
|
||||
switch (status) {
|
||||
case "pending":
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-yellow-500/20 text-yellow-700 dark:text-yellow-400">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-yellow-500/20 text-yellow-700 dark:text-yellow-400"
|
||||
>
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
Pendente
|
||||
</Badge>
|
||||
);
|
||||
case "accepted":
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-green-500/20 text-green-700 dark:text-green-400">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-green-500/20 text-green-700 dark:text-green-400"
|
||||
>
|
||||
<CheckCircle2 className="h-3 w-3 mr-1" />
|
||||
Aceita
|
||||
</Badge>
|
||||
);
|
||||
case "rejected":
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-red-500/20 text-red-700 dark:text-red-400">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-red-500/20 text-red-700 dark:text-red-400"
|
||||
>
|
||||
<XCircle className="h-3 w-3 mr-1" />
|
||||
Rejeitada
|
||||
</Badge>
|
||||
);
|
||||
case "expired":
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-gray-500/20 text-gray-700 dark:text-gray-400">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-gray-500/20 text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
<AlertCircle className="h-3 w-3 mr-1" />
|
||||
Expirada
|
||||
</Badge>
|
||||
);
|
||||
case "completed":
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-blue-500/20 text-blue-700 dark:text-blue-400">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-blue-500/20 text-blue-700 dark:text-blue-400"
|
||||
>
|
||||
<CheckCircle2 className="h-3 w-3 mr-1" />
|
||||
Concluída
|
||||
</Badge>
|
||||
@@ -110,18 +130,18 @@ function getPaymentMethodLabel(method: string): string {
|
||||
|
||||
function formatPrice(price: number | null, method: string): string {
|
||||
if (price === null) return "N/A";
|
||||
|
||||
|
||||
if (method === "pix") {
|
||||
return new Intl.NumberFormat("pt-BR", {
|
||||
style: "currency",
|
||||
currency: "BRL",
|
||||
}).format(price);
|
||||
}
|
||||
|
||||
|
||||
if (method === "tibia_coins") {
|
||||
return `${price.toLocaleString("pt-BR")} TC`;
|
||||
}
|
||||
|
||||
|
||||
if (method === "gold_coins") {
|
||||
const gold = price / 1000000;
|
||||
if (gold >= 1) {
|
||||
@@ -129,11 +149,14 @@ function formatPrice(price: number | null, method: string): string {
|
||||
}
|
||||
return `${price.toLocaleString("pt-BR")} gp`;
|
||||
}
|
||||
|
||||
|
||||
return price.toLocaleString("pt-BR");
|
||||
}
|
||||
|
||||
function formatCustomOffer(amount: number | null | undefined, currency: string | null | undefined): string | null {
|
||||
function formatCustomOffer(
|
||||
amount: number | null | undefined,
|
||||
currency: string | null | undefined,
|
||||
): string | null {
|
||||
if (amount === null || amount === undefined || !currency) {
|
||||
return null;
|
||||
}
|
||||
@@ -171,7 +194,9 @@ function IntentionCard({ intention, locale = "pt" }: IntentionCardProps) {
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-semibold text-lg">Anúncio não disponível</h3>
|
||||
<h3 className="font-semibold text-lg">
|
||||
Anúncio não disponível
|
||||
</h3>
|
||||
{getStatusBadge(intention.status)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@@ -203,7 +228,9 @@ function IntentionCard({ intention, locale = "pt" }: IntentionCardProps) {
|
||||
return `/api/images?path=${encodeURIComponent(`data/images/${category}/${normalizedPath}`)}`;
|
||||
}
|
||||
if (listing.item_id_unified) {
|
||||
const params = new URLSearchParams({ item: listing.item_id_unified.toString() });
|
||||
const params = new URLSearchParams({
|
||||
item: listing.item_id_unified.toString(),
|
||||
});
|
||||
if (listing.item_name) {
|
||||
params.set("name", listing.item_name);
|
||||
}
|
||||
@@ -223,24 +250,27 @@ function IntentionCard({ intention, locale = "pt" }: IntentionCardProps) {
|
||||
? `${listing.listing_id}_${intention.buyer_id}_${listing.seller_id}`
|
||||
: null);
|
||||
|
||||
const price = listing.price_pix && intention.payment_method === "pix"
|
||||
? listing.price_pix
|
||||
: listing.price_tibia_coins && intention.payment_method === "tibia_coins"
|
||||
? listing.price_tibia_coins
|
||||
: listing.price_gold_coins && intention.payment_method === "gold_coins"
|
||||
? listing.price_gold_coins
|
||||
: null;
|
||||
const price =
|
||||
listing.price_pix && intention.payment_method === "pix"
|
||||
? listing.price_pix
|
||||
: listing.price_tibia_coins && intention.payment_method === "tibia_coins"
|
||||
? listing.price_tibia_coins
|
||||
: listing.price_gold_coins && intention.payment_method === "gold_coins"
|
||||
? listing.price_gold_coins
|
||||
: null;
|
||||
const customOfferLabel = formatCustomOffer(
|
||||
typeof intention.custom_offer_amount === "number" ? intention.custom_offer_amount : null,
|
||||
intention.custom_offer_currency ?? null
|
||||
typeof intention.custom_offer_amount === "number"
|
||||
? intention.custom_offer_amount
|
||||
: null,
|
||||
intention.custom_offer_currency ?? null,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="hover:shadow-md transition-shadow">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-4 flex-1 min-w-0">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-4 flex-1 min-w-0">
|
||||
<div className="relative w-16 h-16 rounded-lg bg-muted flex items-center justify-center flex-shrink-0 overflow-hidden">
|
||||
{itemImageUrl ? (
|
||||
<Image
|
||||
@@ -254,160 +284,172 @@ function IntentionCard({ intention, locale = "pt" }: IntentionCardProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-semibold text-lg truncate">{listing.item_name}</h3>
|
||||
{getStatusBadge(intention.status)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground mb-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<MapPin className="h-3 w-3" />
|
||||
<span>{listing.server_name}</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-semibold text-lg truncate">
|
||||
{listing.item_name}
|
||||
</h3>
|
||||
{getStatusBadge(intention.status)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Package className="h-3 w-3" />
|
||||
<span>Qtd: {listing.quantity}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<ShoppingCart className="h-3 w-3" />
|
||||
<span>{getPaymentMethodLabel(intention.payment_method)}</span>
|
||||
{price !== null && (
|
||||
<span className="font-medium text-foreground ml-1">
|
||||
{formatPrice(price, intention.payment_method)}
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground mb-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<MapPin className="h-3 w-3" />
|
||||
<span>{listing.server_name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Package className="h-3 w-3" />
|
||||
<span>Qtd: {listing.quantity}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<ShoppingCart className="h-3 w-3" />
|
||||
<span>
|
||||
{getPaymentMethodLabel(intention.payment_method)}
|
||||
</span>
|
||||
{price !== null && (
|
||||
<span className="font-medium text-foreground ml-1">
|
||||
{formatPrice(price, intention.payment_method)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{customOfferLabel && (
|
||||
<div className="flex items-center gap-1 text-foreground">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-primary text-primary"
|
||||
>
|
||||
Sua oferta: {customOfferLabel}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{customOfferLabel && (
|
||||
<div className="flex items-center gap-1 text-foreground">
|
||||
<Badge variant="outline" className="border-primary text-primary">
|
||||
Sua oferta: {customOfferLabel}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-3">
|
||||
{/* Informações do vendedor */}
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<User className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Vendedor:</span>
|
||||
<span className="font-medium">
|
||||
{listing.seller_character_name || listing.seller_username || "Desconhecido"}
|
||||
</span>
|
||||
</div>
|
||||
<CardContent className="space-y-3">
|
||||
{/* Informações do vendedor */}
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<User className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Vendedor:</span>
|
||||
<span className="font-medium">
|
||||
{listing.seller_character_name ||
|
||||
listing.seller_username ||
|
||||
"Desconhecido"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Mensagem do comprador */}
|
||||
{intention.buyer_message && (
|
||||
<div className="bg-muted/50 rounded-md p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<MessageSquare className="h-4 w-4 text-muted-foreground mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs text-muted-foreground mb-1">Sua mensagem:</p>
|
||||
<p className="text-sm">{intention.buyer_message}</p>
|
||||
{/* Mensagem do comprador */}
|
||||
{intention.buyer_message && (
|
||||
<div className="bg-muted/50 rounded-md p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<MessageSquare className="h-4 w-4 text-muted-foreground mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs text-muted-foreground mb-1">
|
||||
Sua mensagem:
|
||||
</p>
|
||||
<p className="text-sm">{intention.buyer_message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Resposta do vendedor */}
|
||||
{intention.seller_response && (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-md p-3 border",
|
||||
intention.status === "accepted"
|
||||
? "bg-green-500/10 border-green-500/20"
|
||||
: intention.status === "rejected"
|
||||
? "bg-red-500/10 border-red-500/20"
|
||||
: "bg-blue-500/10 border-blue-500/20"
|
||||
{/* Resposta do vendedor */}
|
||||
{intention.seller_response && (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-md p-3 border",
|
||||
intention.status === "accepted"
|
||||
? "bg-green-500/10 border-green-500/20"
|
||||
: intention.status === "rejected"
|
||||
? "bg-red-500/10 border-red-500/20"
|
||||
: "bg-blue-500/10 border-blue-500/20",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<MessageSquare
|
||||
className={cn(
|
||||
"h-4 w-4 mt-0.5 flex-shrink-0",
|
||||
intention.status === "accepted"
|
||||
? "text-green-600 dark:text-green-400"
|
||||
: intention.status === "rejected"
|
||||
? "text-red-600 dark:text-red-400"
|
||||
: "text-blue-600 dark:text-blue-400",
|
||||
)}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs font-medium mb-1">
|
||||
{intention.status === "accepted"
|
||||
? "Resposta do vendedor (Aceita):"
|
||||
: intention.status === "rejected"
|
||||
? "Resposta do vendedor (Rejeitada):"
|
||||
: "Resposta do vendedor:"}
|
||||
</p>
|
||||
<p className="text-sm">{intention.seller_response}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timestamps */}
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground pt-2 border-t">
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>Criada {formatTimeAgo(intention.created_at)}</span>
|
||||
</div>
|
||||
{intention.updated_at !== intention.created_at && (
|
||||
<span>Atualizada {formatTimeAgo(intention.updated_at)}</span>
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<MessageSquare
|
||||
className={cn(
|
||||
"h-4 w-4 mt-0.5 flex-shrink-0",
|
||||
intention.status === "accepted"
|
||||
? "text-green-600 dark:text-green-400"
|
||||
: intention.status === "rejected"
|
||||
? "text-red-600 dark:text-red-400"
|
||||
: "text-blue-600 dark:text-blue-400"
|
||||
)}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs font-medium mb-1">
|
||||
{intention.status === "accepted"
|
||||
? "Resposta do vendedor (Aceita):"
|
||||
: intention.status === "rejected"
|
||||
? "Resposta do vendedor (Rejeitada):"
|
||||
: "Resposta do vendedor:"}
|
||||
</p>
|
||||
<p className="text-sm">{intention.seller_response}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timestamps */}
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground pt-2 border-t">
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>Criada {formatTimeAgo(intention.created_at)}</span>
|
||||
</div>
|
||||
{intention.updated_at !== intention.created_at && (
|
||||
<span>Atualizada {formatTimeAgo(intention.updated_at)}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2 pt-2 flex-wrap">
|
||||
{intention.status === "pending" &&
|
||||
intention.payment_method_change_count < 1 && (
|
||||
<ChangePaymentMethodButton
|
||||
intentionId={intention.intention_id}
|
||||
currentPaymentMethod={intention.payment_method}
|
||||
availableMethods={[
|
||||
...(listing.price_pix ? ["pix"] : []),
|
||||
...(listing.price_tibia_coins ? ["tibia_coins"] : []),
|
||||
...(listing.price_gold_coins ? ["gold_coins"] : []),
|
||||
...(listing.trade_item_id ? ["trade"] : []),
|
||||
]}
|
||||
canChange={true}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowListingModal(true)}
|
||||
className="flex-1"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4 mr-2" />
|
||||
Ver Anúncio
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => setShowChatModal(true)}
|
||||
>
|
||||
<MessageSquare className="h-4 w-4 mr-2" />
|
||||
Abrir Chat
|
||||
</Button>
|
||||
{intention.status === "accepted" && listing.listing_status === "active" && (
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2 pt-2 flex-wrap">
|
||||
{intention.status === "pending" &&
|
||||
intention.payment_method_change_count < 1 && (
|
||||
<ChangePaymentMethodButton
|
||||
intentionId={intention.intention_id}
|
||||
currentPaymentMethod={intention.payment_method}
|
||||
availableMethods={[
|
||||
...(listing.price_pix ? ["pix"] : []),
|
||||
...(listing.price_tibia_coins ? ["tibia_coins"] : []),
|
||||
...(listing.price_gold_coins ? ["gold_coins"] : []),
|
||||
...(listing.trade_item_id ? ["trade"] : []),
|
||||
]}
|
||||
canChange={true}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowListingModal(true)}
|
||||
className="flex-1"
|
||||
>
|
||||
<ShoppingCart className="h-4 w-4 mr-2" />
|
||||
Comprar Agora
|
||||
<ExternalLink className="h-4 w-4 mr-2" />
|
||||
Ver Anúncio
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => setShowChatModal(true)}
|
||||
>
|
||||
<MessageSquare className="h-4 w-4 mr-2" />
|
||||
Abrir Chat
|
||||
</Button>
|
||||
{intention.status === "accepted" &&
|
||||
listing.listing_status === "active" && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setShowListingModal(true)}
|
||||
className="flex-1"
|
||||
>
|
||||
<ShoppingCart className="h-4 w-4 mr-2" />
|
||||
Comprar Agora
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Dialog open={showListingModal} onOpenChange={setShowListingModal}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
@@ -434,30 +476,48 @@ function IntentionCard({ intention, locale = "pt" }: IntentionCardProps) {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">{listing.item_name}</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">{listing.description ?? "Sem descrição fornecida para este anúncio."}</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{listing.description ??
|
||||
"Sem descrição fornecida para este anúncio."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<MapPin className="h-4 w-4" />
|
||||
<span>Servidor: <span className="font-medium text-foreground">{listing.server_name}</span></span>
|
||||
<span>
|
||||
Servidor:{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
{listing.server_name}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Package className="h-4 w-4" />
|
||||
<span>Quantidade: <span className="font-medium text-foreground">{listing.quantity}</span></span>
|
||||
<span>
|
||||
Quantidade:{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
{listing.quantity}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<User className="h-4 w-4" />
|
||||
<span>Vendedor:{" "}
|
||||
<span>
|
||||
Vendedor:{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
{listing.seller_character_name || listing.seller_username || "Desconhecido"}
|
||||
{listing.seller_character_name ||
|
||||
listing.seller_username ||
|
||||
"Desconhecido"}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">Condições de pagamento</h4>
|
||||
<h4 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
|
||||
Condições de pagamento
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{listing.price_pix && (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
@@ -477,18 +537,23 @@ function IntentionCard({ intention, locale = "pt" }: IntentionCardProps) {
|
||||
{formatPrice(listing.price_gold_coins, "gold_coins")}
|
||||
</Badge>
|
||||
)}
|
||||
{listing.trade_item_id && listing.trade_item_name && listing.trade_item_quantity && (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
{listing.trade_item_quantity}x {listing.trade_item_name}
|
||||
</Badge>
|
||||
)}
|
||||
{listing.trade_item_id &&
|
||||
listing.trade_item_name &&
|
||||
listing.trade_item_quantity && (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
{listing.trade_item_quantity}x {listing.trade_item_name}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="outline" asChild>
|
||||
<Link href={`/${locale}/market?listing=${listing.listing_id}`} target="_blank">
|
||||
<Link
|
||||
href={`/${locale}/market?listing=${listing.listing_id}`}
|
||||
target="_blank"
|
||||
>
|
||||
Abrir no marketplace
|
||||
</Link>
|
||||
</Button>
|
||||
@@ -521,17 +586,25 @@ interface MyPurchaseIntentionsPanelProps {
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
export function MyPurchaseIntentionsPanel({ locale = "pt" }: MyPurchaseIntentionsPanelProps) {
|
||||
export function MyPurchaseIntentionsPanel({
|
||||
locale = "pt",
|
||||
}: MyPurchaseIntentionsPanelProps) {
|
||||
const { user } = useSession();
|
||||
const [filter, setFilter] = useState<PurchaseIntentionStatus | "all">("all");
|
||||
const { intentions, isLoading } = useMyPurchaseIntentions();
|
||||
|
||||
// Contar por status
|
||||
const pendingCount = intentions.filter((i) => i.status === "pending").length;
|
||||
const acceptedCount = intentions.filter((i) => i.status === "accepted").length;
|
||||
const rejectedCount = intentions.filter((i) => i.status === "rejected").length;
|
||||
const acceptedCount = intentions.filter(
|
||||
(i) => i.status === "accepted",
|
||||
).length;
|
||||
const rejectedCount = intentions.filter(
|
||||
(i) => i.status === "rejected",
|
||||
).length;
|
||||
const expiredCount = intentions.filter((i) => i.status === "expired").length;
|
||||
const completedCount = intentions.filter((i) => i.status === "completed").length;
|
||||
const completedCount = intentions.filter(
|
||||
(i) => i.status === "completed",
|
||||
).length;
|
||||
const filteredIntentions =
|
||||
filter === "all"
|
||||
? intentions
|
||||
@@ -540,7 +613,9 @@ export function MyPurchaseIntentionsPanel({ locale = "pt" }: MyPurchaseIntention
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-muted-foreground">Faça login para ver suas intenções de compra</p>
|
||||
<p className="text-muted-foreground">
|
||||
Faça login para ver suas intenções de compra
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -614,7 +689,9 @@ export function MyPurchaseIntentionsPanel({ locale = "pt" }: MyPurchaseIntention
|
||||
) : filteredIntentions.length === 0 ? (
|
||||
<div className="text-center py-12 border rounded-lg">
|
||||
<ShoppingCart className="h-12 w-12 mx-auto mb-4 text-muted-foreground opacity-50" />
|
||||
<p className="text-lg font-medium mb-2">Nenhuma intenção de compra encontrada</p>
|
||||
<p className="text-lg font-medium mb-2">
|
||||
Nenhuma intenção de compra encontrada
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{filter === "all"
|
||||
? "Quando você manifestar interesse em comprar um item, ele aparecerá aqui."
|
||||
@@ -627,11 +704,14 @@ export function MyPurchaseIntentionsPanel({ locale = "pt" }: MyPurchaseIntention
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{filteredIntentions.map((intention) => (
|
||||
<IntentionCard key={intention.intention_id} intention={intention} locale={locale} />
|
||||
<IntentionCard
|
||||
key={intention.intention_id}
|
||||
intention={intention}
|
||||
locale={locale}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { Coins, DollarSign, Loader2, RefreshCw } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -8,13 +11,10 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { DollarSign, Coins, RefreshCw, Loader2 } from "lucide-react";
|
||||
import { useSession } from "@/lib/hooks/use-session";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
interface Listing {
|
||||
listing_id: number;
|
||||
@@ -37,7 +37,12 @@ interface PurchaseModalProps {
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: PurchaseModalProps) {
|
||||
export function PurchaseModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
listing,
|
||||
onSuccess,
|
||||
}: PurchaseModalProps) {
|
||||
const { user } = useSession();
|
||||
const [selectedMethod, setSelectedMethod] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -53,7 +58,10 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedMethod && !["pix", "tibia_coins", "gold_coins"].includes(selectedMethod)) {
|
||||
if (
|
||||
selectedMethod &&
|
||||
!["pix", "tibia_coins", "gold_coins"].includes(selectedMethod)
|
||||
) {
|
||||
setCustomOfferValue("");
|
||||
}
|
||||
}, [selectedMethod]);
|
||||
@@ -61,13 +69,19 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
|
||||
if (!listing) return null;
|
||||
|
||||
// Determinar métodos de pagamento disponíveis
|
||||
const availableMethods: Array<{ type: string; label: string; value: string; icon: any }> = [];
|
||||
|
||||
const availableMethods: Array<{
|
||||
type: string;
|
||||
label: string;
|
||||
value: string;
|
||||
icon: any;
|
||||
}> = [];
|
||||
|
||||
if (listing.price_pix) {
|
||||
const price = typeof listing.price_pix === 'string'
|
||||
? parseFloat(listing.price_pix)
|
||||
: listing.price_pix;
|
||||
if (!isNaN(price)) {
|
||||
const price =
|
||||
typeof listing.price_pix === "string"
|
||||
? parseFloat(listing.price_pix)
|
||||
: listing.price_pix;
|
||||
if (!Number.isNaN(price)) {
|
||||
availableMethods.push({
|
||||
type: "pix",
|
||||
label: "PIX",
|
||||
@@ -78,10 +92,11 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
|
||||
}
|
||||
|
||||
if (listing.price_tibia_coins) {
|
||||
const price = typeof listing.price_tibia_coins === 'string'
|
||||
? parseInt(listing.price_tibia_coins, 10)
|
||||
: listing.price_tibia_coins;
|
||||
if (!isNaN(price)) {
|
||||
const price =
|
||||
typeof listing.price_tibia_coins === "string"
|
||||
? parseInt(listing.price_tibia_coins, 10)
|
||||
: listing.price_tibia_coins;
|
||||
if (!Number.isNaN(price)) {
|
||||
availableMethods.push({
|
||||
type: "tibia_coins",
|
||||
label: "Tibia Coins",
|
||||
@@ -92,10 +107,11 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
|
||||
}
|
||||
|
||||
if (listing.price_gold_coins) {
|
||||
const price = typeof listing.price_gold_coins === 'string'
|
||||
? parseInt(listing.price_gold_coins, 10)
|
||||
: listing.price_gold_coins;
|
||||
if (!isNaN(price)) {
|
||||
const price =
|
||||
typeof listing.price_gold_coins === "string"
|
||||
? parseInt(listing.price_gold_coins, 10)
|
||||
: listing.price_gold_coins;
|
||||
if (!Number.isNaN(price)) {
|
||||
availableMethods.push({
|
||||
type: "gold_coins",
|
||||
label: "Gold Coins",
|
||||
@@ -105,7 +121,11 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
|
||||
}
|
||||
}
|
||||
|
||||
if (listing.trade_item_id && listing.trade_item_name && listing.trade_item_quantity) {
|
||||
if (
|
||||
listing.trade_item_id &&
|
||||
listing.trade_item_name &&
|
||||
listing.trade_item_quantity
|
||||
) {
|
||||
availableMethods.push({
|
||||
type: "trade",
|
||||
label: "Troca",
|
||||
@@ -114,7 +134,10 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
|
||||
});
|
||||
}
|
||||
|
||||
const formatOffer = (amount: number | string | null, currency: string | null) => {
|
||||
const formatOffer = (
|
||||
amount: number | string | null,
|
||||
currency: string | null,
|
||||
) => {
|
||||
if (amount === null || amount === undefined || !currency) {
|
||||
return null;
|
||||
}
|
||||
@@ -136,7 +159,7 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
|
||||
|
||||
const highestOfferLabel = formatOffer(
|
||||
listing.highest_custom_offer_amount ?? null,
|
||||
listing.highest_custom_offer_currency ?? null
|
||||
listing.highest_custom_offer_currency ?? null,
|
||||
);
|
||||
|
||||
// Se houver apenas um método, selecionar automaticamente
|
||||
@@ -167,20 +190,28 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
|
||||
payment_method: selectedMethod,
|
||||
};
|
||||
|
||||
if (hasCustomOffer && ["pix", "tibia_coins", "gold_coins"].includes(selectedMethod)) {
|
||||
if (
|
||||
hasCustomOffer &&
|
||||
["pix", "tibia_coins", "gold_coins"].includes(selectedMethod)
|
||||
) {
|
||||
const offerNumber = Number(customOfferValue);
|
||||
payload.custom_offer_amount =
|
||||
selectedMethod === "pix" ? Number(offerNumber.toFixed(2)) : Math.floor(offerNumber);
|
||||
selectedMethod === "pix"
|
||||
? Number(offerNumber.toFixed(2))
|
||||
: Math.floor(offerNumber);
|
||||
payload.custom_offer_currency = selectedMethod;
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/listings/${listing.listing_id}/purchase`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
const response = await fetch(
|
||||
`/api/listings/${listing.listing_id}/purchase`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
@@ -236,10 +267,16 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">Valores anunciados pelo vendedor</p>
|
||||
<p className="text-sm font-medium">
|
||||
Valores anunciados pelo vendedor
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{availableMethods.map((method) => (
|
||||
<Badge key={`original-${method.type}`} variant="secondary" className="gap-1">
|
||||
<Badge
|
||||
key={`original-${method.type}`}
|
||||
variant="secondary"
|
||||
className="gap-1"
|
||||
>
|
||||
{method.label}: {method.value}
|
||||
</Badge>
|
||||
))}
|
||||
@@ -287,15 +324,21 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
|
||||
) : availableMethods.length === 1 ? (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Método de Pagamento</Label>
|
||||
<div className={`p-4 rounded-lg border-2 ${getMethodClassName(availableMethods[0].type)}`}>
|
||||
<div
|
||||
className={`p-4 rounded-lg border-2 ${getMethodClassName(availableMethods[0].type)}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
{(() => {
|
||||
const Icon = availableMethods[0].icon;
|
||||
return <Icon className="h-4 w-4" />;
|
||||
})()}
|
||||
<span className="font-medium">{availableMethods[0].label}</span>
|
||||
<span className="font-medium">
|
||||
{availableMethods[0].label}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm opacity-90">{availableMethods[0].value}</p>
|
||||
<p className="text-sm opacity-90">
|
||||
{availableMethods[0].value}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -308,7 +351,8 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
|
||||
["pix", "tibia_coins", "gold_coins"].includes(selectedMethod) && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">
|
||||
Propor valor personalizado ({selectedMethod === "pix" ? "R$" : "quantidade"})
|
||||
Propor valor personalizado (
|
||||
{selectedMethod === "pix" ? "R$" : "quantidade"})
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
@@ -316,11 +360,13 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
|
||||
step={selectedMethod === "pix" ? "0.01" : "1"}
|
||||
value={customOfferValue}
|
||||
onChange={(event) => setCustomOfferValue(event.target.value)}
|
||||
placeholder={selectedMethod === "pix" ? "Ex.: 199.90" : "Ex.: 1200"}
|
||||
placeholder={
|
||||
selectedMethod === "pix" ? "Ex.: 199.90" : "Ex.: 1200"
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Esse valor será enviado ao vendedor como sua proposta. Ele pode aceitar, negar ou
|
||||
responder.
|
||||
Esse valor será enviado ao vendedor como sua proposta. Ele
|
||||
pode aceitar, negar ou responder.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -344,7 +390,9 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handlePurchase}
|
||||
disabled={loading || !selectedMethod || availableMethods.length === 0}
|
||||
disabled={
|
||||
loading || !selectedMethod || availableMethods.length === 0
|
||||
}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
@@ -361,4 +409,3 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { CheckCircle2, User } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { User, CheckCircle2 } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useMarketTelemetry } from "@/lib/hooks/use-telemetry";
|
||||
|
||||
interface SellerInfoProps {
|
||||
@@ -27,14 +27,10 @@ export function SellerInfo({
|
||||
|
||||
// Tracking de visualização de vendedor
|
||||
useEffect(() => {
|
||||
trackCustomEvent(
|
||||
"seller_profile_viewed",
|
||||
listingId,
|
||||
{
|
||||
seller_id: sellerId,
|
||||
seller_name: sellerName,
|
||||
}
|
||||
);
|
||||
trackCustomEvent("seller_profile_viewed", listingId, {
|
||||
seller_id: sellerId,
|
||||
seller_name: sellerName,
|
||||
});
|
||||
}, [sellerId, sellerName, listingId, trackCustomEvent]);
|
||||
|
||||
// Gerar iniciais do nome
|
||||
@@ -75,4 +71,3 @@ export function SellerInfo({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user