feat: enhance purchase intentions with chat integration and offer tracking

- Added MongoDB chat/message integration for seller responses
- Improved seller response validation and default messaging
- Integrated automatic chat creation/update when responding to intentions
- Added highest custom offer fields to listing queries and models
- Enhanced listing data with top offer information for buyers
This commit is contained in:
Evandro Fonseca Junior committed 2025-11-08 11:51:21 -03:00
1 parent d27526e026
commit d8e7d5d7e6
19 files changed
+806 -58

No files matched your search

+50
View File
@@ -0,0 +1,50 @@
"use client";
import { AdminSubscriptionDashboard } from "@/components/admin/subscription-dashboard";
import { Card, CardContent } from "@/components/ui/card";
interface AdminSubscriptionTabProps {
initialTiers: any[];
adminUser: {
subscription_tier?: {
name: string;
slug: string;
} | null;
} | null;
}
export function AdminSubscriptionTab({
initialTiers,
adminUser,
}: AdminSubscriptionTabProps) {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="space-y-2">
<h2 className="text-2xl font-semibold tracking-tight">
Assinaturas & Permissões
</h2>
<p className="text-sm text-muted-foreground max-w-2xl">
Configure planos, recursos e vincule usuários às assinaturas
disponíveis. Todas as alterações são aplicadas imediatamente
após salvar.
</p>
{adminUser?.subscription_tier && (
<p className="text-xs text-muted-foreground">
Seu plano atual:{" "}
<span className="font-semibold uppercase">
{adminUser.subscription_tier.name} (
{adminUser.subscription_tier.slug})
</span>
</p>
)}
</div>
</CardContent>
</Card>
<AdminSubscriptionDashboard initialTiers={initialTiers} />
</div>
);
}
+54 -9
View File
@@ -68,10 +68,12 @@ interface Listing {
// Campo do vendedor
seller_character_name?: string | null;
has_pending_intention_for_current_user?: boolean;
highest_custom_offer_amount?: number | string | null;
highest_custom_offer_currency?: string | null;
}
interface PriceInfo {
type: "pix" | "tc" | "gp" | "trade";
type: "pix" | "tibia_coins" | "gold_coins" | "trade";
label: string;
value: string;
}
@@ -227,7 +229,7 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
: listing.price_tibia_coins;
if (!isNaN(price)) {
prices.push({
type: "tc",
type: "tibia_coins",
label: "TC",
value: `${price.toLocaleString()} TC`,
});
@@ -241,7 +243,7 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
: listing.price_gold_coins;
if (!isNaN(price)) {
prices.push({
type: "gp",
type: "gold_coins",
label: "GP",
value: `${price.toLocaleString()} GP`,
});
@@ -266,6 +268,35 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
const isSold = listing.listing_status === "sold";
const hasPendingIntention = Boolean(listing.has_pending_intention_for_current_user);
const getHighestOfferLabel = () => {
if (
listing.highest_custom_offer_amount === null ||
listing.highest_custom_offer_amount === undefined ||
!listing.highest_custom_offer_currency
) {
return null;
}
const amount =
typeof listing.highest_custom_offer_amount === "string"
? Number(listing.highest_custom_offer_amount)
: listing.highest_custom_offer_amount;
if (Number.isNaN(amount)) {
return null;
}
switch (listing.highest_custom_offer_currency) {
case "pix":
return `Maior oferta: R$ ${amount.toFixed(2)}`;
case "tibia_coins":
return `Maior oferta: ${Math.round(amount).toLocaleString()} TC`;
case "gold_coins":
return `Maior oferta: ${Math.round(amount).toLocaleString()} GP`;
default:
return `Maior oferta: ${amount}`;
}
};
const highestOfferLabel = getHighestOfferLabel();
// Função para obter URL da imagem do item
const getItemImageUrl = (): string => {
// Prioridade 1: image_url (URL externa) se disponível
@@ -327,9 +358,9 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
switch (type) {
case "pix":
return "bg-green-100 hover:bg-green-200 dark:bg-green-900/30 dark:hover:bg-green-900/50 text-green-700 dark:text-green-300 border-green-300 dark:border-green-700";
case "tc":
case "tibia_coins":
return "bg-blue-100 hover:bg-blue-200 dark:bg-blue-900/30 dark:hover:bg-blue-900/50 text-blue-700 dark:text-blue-300 border-blue-300 dark:border-blue-700";
case "gp":
case "gold_coins":
return "bg-amber-100 hover:bg-amber-200 dark:bg-amber-900/30 dark:hover:bg-amber-900/50 text-amber-700 dark:text-amber-300 border-amber-300 dark:border-amber-700";
case "trade":
return "bg-purple-100 hover:bg-purple-200 dark:bg-purple-900/30 dark:hover:bg-purple-900/50 text-purple-700 dark:text-purple-300 border-purple-300 dark:border-purple-700";
@@ -342,8 +373,8 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
switch (type) {
case "pix":
return <DollarSign className="h-3 w-3" />;
case "tc":
case "gp":
case "tibia_coins":
case "gold_coins":
return <Coins className="h-3 w-3" />;
case "trade":
return <RefreshCw className="h-3 w-3" />;
@@ -554,7 +585,14 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
listing.is_featured && "p-2 rounded-lg border-l-4 bg-yellow-500/5 border-yellow-500",
"overflow-hidden"
)}>
<PriceBadges prices={availablePrices} compact tradeItemImageUrl={tradeItemImageUrl} listing={listing} />
<div className="space-y-1">
<PriceBadges prices={availablePrices} compact tradeItemImageUrl={tradeItemImageUrl} listing={listing} />
{highestOfferLabel && (
<div className="text-xs font-medium text-primary/80">
{highestOfferLabel}
</div>
)}
</div>
</div>
)}
@@ -846,7 +884,14 @@ export function AdCard({ listing, viewMode = "grid" }: AdCardProps) {
listing.is_featured && "p-2 rounded-lg border-l-4 bg-yellow-500/5 border-yellow-500",
"overflow-hidden"
)}>
<PriceBadges prices={availablePrices} tradeItemImageUrl={tradeItemImageUrl} listing={listing} />
<div className="space-y-1">
<PriceBadges prices={availablePrices} tradeItemImageUrl={tradeItemImageUrl} listing={listing} />
{highestOfferLabel && (
<div className="text-xs font-medium text-primary/80">
{highestOfferLabel}
</div>
)}
</div>
</div>
)}
+25 -1
View File
@@ -139,7 +139,31 @@ export function CreateAdModal({ open, onOpenChange, onSuccess }: CreateAdModalPr
const data = await response.json();
if (!response.ok) {
setError(data.error || "Erro ao criar anúncio");
if (data?.details?.length) {
const fieldLabels: Record<string, string> = {
item_id: "Item",
item_id_unified: "Item",
item_name: "Nome do item",
quantity: "Quantidade",
server_name: "Servidor",
price_pix: "Preço PIX",
price_tibia_coins: "Preço Tibia Coins",
price_gold_coins: "Preço Gold Coins",
trade_item_id: "Item de troca",
trade_item_name: "Nome do item de troca",
trade_item_quantity: "Quantidade do item de troca",
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"}`;
});
setError(validationMessages.join(" "));
} else {
setError(data?.error || "Erro ao criar anúncio");
}
setLoading(false);
return;
}
@@ -35,6 +35,9 @@ interface PurchaseIntention {
buyer_message: string | null;
seller_response: string | null;
created_at: string;
custom_offer_amount: number | null;
custom_offer_currency: string | null;
custom_offer_created_at: string | null;
buyer?: {
user_id: number;
username: string;
@@ -93,6 +96,40 @@ function getStatusBadge(status: string) {
}
}
function formatCustomOffer(intention: PurchaseIntention): { value: string; date: string | null } | null {
if (
intention.custom_offer_amount === null ||
intention.custom_offer_amount === undefined ||
!intention.custom_offer_currency
) {
return null;
}
const amount = intention.custom_offer_amount;
let value: string;
switch (intention.custom_offer_currency) {
case "pix":
value = `R$ ${amount.toFixed(2)}`;
break;
case "tibia_coins":
value = `${Math.round(amount).toLocaleString("pt-BR")} TC`;
break;
case "gold_coins":
value = `${Math.round(amount).toLocaleString("pt-BR")} GP`;
break;
default:
value = amount.toLocaleString("pt-BR");
}
return {
value,
date: intention.custom_offer_created_at
? new Date(intention.custom_offer_created_at).toLocaleString("pt-BR")
: null,
};
}
export function IntentionDetailModal({
open,
onOpenChange,
@@ -126,6 +163,13 @@ export function IntentionDetailModal({
const isOwner = user && listing && user.userId === listing.seller_id;
const canMarkAsSold = isOwner && listing && (listing.listing_status === "active" || listing.listing_status === "in_escrow");
const canAcceptOrReject = intention.status === "pending";
const customOffer = formatCustomOffer(intention);
useEffect(() => {
if (open) {
setSellerResponse(intention.seller_response ?? "");
}
}, [open, intention.intention_id, intention.seller_response]);
const handleAccept = async () => {
setLoading(true);
@@ -279,6 +323,18 @@ export function IntentionDetailModal({
</div>
</div>
{customOffer && (
<div className="bg-primary/5 border border-primary/20 rounded-lg p-3 space-y-1">
<Label className="text-xs text-primary uppercase tracking-wide">
Oferta do Comprador
</Label>
<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>
)}
</div>
)}
{/* Mensagem do comprador */}
{intention.buyer_message && (
<div>
+37 -4
View File
@@ -61,6 +61,8 @@ interface Listing {
image_url?: string | null;
item_category?: string | null;
seller_character_name?: string | null;
highest_custom_offer_amount?: number | string | null;
highest_custom_offer_currency?: string | null;
}
interface ListingDetailsModalProps {
@@ -165,14 +167,14 @@ export function ListingDetailsModal({
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: "tc", label: "Tibia Coins", value: `${price.toLocaleString()} TC` });
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: "gp", label: "Gold Coins", value: `${price.toLocaleString()} GP` });
prices.push({ type: "gold_coins", label: "Gold Coins", value: `${price.toLocaleString()} GP` });
}
}
@@ -188,6 +190,32 @@ export function ListingDetailsModal({
};
const prices = getAvailablePrices();
const highestOfferLabel = (() => {
if (
listing.highest_custom_offer_amount === null ||
listing.highest_custom_offer_amount === undefined ||
!listing.highest_custom_offer_currency
) {
return null;
}
const amount =
typeof listing.highest_custom_offer_amount === "string"
? Number(listing.highest_custom_offer_amount)
: listing.highest_custom_offer_amount;
if (Number.isNaN(amount)) {
return null;
}
switch (listing.highest_custom_offer_currency) {
case "pix":
return `Maior oferta registrada: R$ ${amount.toFixed(2)}`;
case "tibia_coins":
return `Maior oferta registrada: ${Math.round(amount).toLocaleString()} TC`;
case "gold_coins":
return `Maior oferta registrada: ${Math.round(amount).toLocaleString()} GP`;
default:
return `Maior oferta registrada: ${amount}`;
}
})();
// Apenas o dono pode realizar ações
const canPause = isOwner && listing.listing_status === "active";
const canActivate = isOwner && listing.listing_status === "cancelled";
@@ -314,9 +342,9 @@ export function ListingDetailsModal({
className={`p-3 rounded-lg border ${
price.type === "pix"
? "bg-green-500/10 border-green-500/20"
: price.type === "tc"
: price.type === "tibia_coins"
? "bg-blue-500/10 border-blue-500/20"
: price.type === "gp"
: price.type === "gold_coins"
? "bg-yellow-500/10 border-yellow-500/20"
: "bg-purple-500/10 border-purple-500/20"
}`}
@@ -326,6 +354,11 @@ export function ListingDetailsModal({
</div>
))}
</div>
{highestOfferLabel && (
<div className="rounded-lg border border-primary/30 bg-primary/5 px-3 py-2 text-sm font-medium text-primary">
{highestOfferLabel}
</div>
)}
</div>
{/* Informações de leilão */}
@@ -16,6 +16,9 @@ interface PurchaseIntention {
buyer_message: string | null;
seller_response: string | null;
created_at: string;
custom_offer_amount: number | null;
custom_offer_currency: string | null;
custom_offer_created_at: string | null;
buyer?: {
user_id: number;
username: string;
@@ -82,6 +85,28 @@ function getPaymentMethodLabel(method: string): string {
}
}
function formatCustomOffer(intention: PurchaseIntention): string | null {
if (
intention.custom_offer_amount === null ||
intention.custom_offer_amount === undefined ||
!intention.custom_offer_currency
) {
return null;
}
const amount = intention.custom_offer_amount;
switch (intention.custom_offer_currency) {
case "pix":
return `R$ ${amount.toFixed(2)}`;
case "tibia_coins":
return `${Math.round(amount).toLocaleString("pt-BR")} TC`;
case "gold_coins":
return `${Math.round(amount).toLocaleString("pt-BR")} GP`;
default:
return amount.toLocaleString("pt-BR");
}
}
export function MyAdsIntentionList({ listingId, sellerId }: MyAdsIntentionListProps) {
const [intentions, setIntentions] = useState<PurchaseIntention[]>([]);
const [loading, setLoading] = useState(true);
@@ -196,6 +221,18 @@ export function MyAdsIntentionList({ listingId, sellerId }: MyAdsIntentionListPr
<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">
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")}
</span>
)}
</div>
)}
{intention.buyer_message && (
<p className="text-xs text-muted-foreground line-clamp-1">
<MessageSquare className="h-3 w-3 inline mr-1" />
@@ -133,6 +133,23 @@ function formatPrice(price: number | null, method: string): string {
return price.toLocaleString("pt-BR");
}
function formatCustomOffer(amount: number | null | undefined, currency: string | null | undefined): string | null {
if (amount === null || amount === undefined || !currency) {
return null;
}
switch (currency) {
case "pix":
return `R$ ${amount.toFixed(2)}`;
case "tibia_coins":
return `${Math.round(amount).toLocaleString("pt-BR")} TC`;
case "gold_coins":
return `${Math.round(amount).toLocaleString("pt-BR")} GP`;
default:
return amount.toLocaleString("pt-BR");
}
}
interface IntentionCardProps {
intention: PurchaseIntention;
locale?: string;
@@ -213,6 +230,10 @@ function IntentionCard({ intention, locale = "pt" }: IntentionCardProps) {
: 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
);
return (
<>
@@ -256,6 +277,13 @@ function IntentionCard({ intention, locale = "pt" }: IntentionCardProps) {
</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>
</div>
</div>
@@ -496,10 +524,7 @@ interface MyPurchaseIntentionsPanelProps {
export function MyPurchaseIntentionsPanel({ locale = "pt" }: MyPurchaseIntentionsPanelProps) {
const { user } = useSession();
const [filter, setFilter] = useState<PurchaseIntentionStatus | "all">("all");
const { intentions, isLoading, refetch } = useMyPurchaseIntentions(
filter === "all" ? undefined : filter
);
const { intentions, isLoading } = useMyPurchaseIntentions();
// Contar por status
const pendingCount = intentions.filter((i) => i.status === "pending").length;
@@ -507,6 +532,10 @@ export function MyPurchaseIntentionsPanel({ locale = "pt" }: MyPurchaseIntention
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 filteredIntentions =
filter === "all"
? intentions
: intentions.filter((intention) => intention.status === filter);
if (!user) {
return (
@@ -582,7 +611,7 @@ export function MyPurchaseIntentionsPanel({ locale = "pt" }: MyPurchaseIntention
<Skeleton key={i} className="h-48 w-full" />
))}
</div>
) : intentions.length === 0 ? (
) : 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>
@@ -597,7 +626,7 @@ export function MyPurchaseIntentionsPanel({ locale = "pt" }: MyPurchaseIntention
</div>
) : (
<div className="space-y-4">
{intentions.map((intention) => (
{filteredIntentions.map((intention) => (
<IntentionCard key={intention.intention_id} intention={intention} locale={locale} />
))}
</div>
+109 -8
View File
@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import {
Dialog,
DialogContent,
@@ -14,6 +14,7 @@ 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;
@@ -25,6 +26,8 @@ interface Listing {
trade_item_id?: number | null;
trade_item_name?: string | null;
trade_item_quantity?: number | null;
highest_custom_offer_amount?: number | string | null;
highest_custom_offer_currency?: string | null;
}
interface PurchaseModalProps {
@@ -39,6 +42,21 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
const [selectedMethod, setSelectedMethod] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [customOfferValue, setCustomOfferValue] = useState<string>("");
useEffect(() => {
if (!open) {
setSelectedMethod(null);
setCustomOfferValue("");
setError(null);
}
}, [open]);
useEffect(() => {
if (selectedMethod && !["pix", "tibia_coins", "gold_coins"].includes(selectedMethod)) {
setCustomOfferValue("");
}
}, [selectedMethod]);
if (!listing) return null;
@@ -65,7 +83,7 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
: listing.price_tibia_coins;
if (!isNaN(price)) {
availableMethods.push({
type: "tc",
type: "tibia_coins",
label: "Tibia Coins",
value: `${price.toLocaleString()} TC`,
icon: Coins,
@@ -79,7 +97,7 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
: listing.price_gold_coins;
if (!isNaN(price)) {
availableMethods.push({
type: "gp",
type: "gold_coins",
label: "Gold Coins",
value: `${price.toLocaleString()} GP`,
icon: Coins,
@@ -96,6 +114,31 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
});
}
const formatOffer = (amount: number | string | null, currency: string | null) => {
if (amount === null || amount === undefined || !currency) {
return null;
}
const numeric = typeof amount === "string" ? Number(amount) : amount;
if (Number.isNaN(numeric)) {
return null;
}
switch (currency) {
case "pix":
return `R$ ${numeric.toFixed(2)}`;
case "tibia_coins":
return `${Math.round(numeric).toLocaleString()} TC`;
case "gold_coins":
return `${Math.round(numeric).toLocaleString()} GP`;
default:
return `${numeric}`;
}
};
const highestOfferLabel = formatOffer(
listing.highest_custom_offer_amount ?? null,
listing.highest_custom_offer_currency ?? null
);
// Se houver apenas um método, selecionar automaticamente
if (availableMethods.length === 1 && !selectedMethod) {
setSelectedMethod(availableMethods[0].type);
@@ -107,18 +150,36 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
return;
}
const hasCustomOffer = customOfferValue.trim().length > 0;
if (hasCustomOffer) {
const offerNumber = Number(customOfferValue);
if (Number.isNaN(offerNumber) || offerNumber <= 0) {
setError("Informe um valor válido para a oferta personalizada.");
return;
}
}
setLoading(true);
setError(null);
try {
const payload: Record<string, unknown> = {
payment_method: 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);
payload.custom_offer_currency = selectedMethod;
}
const response = await fetch(`/api/listings/${listing.listing_id}/purchase`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
payment_method: selectedMethod,
}),
body: JSON.stringify(payload),
});
const data = await response.json();
@@ -132,6 +193,7 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
onSuccess();
}
onOpenChange(false);
setCustomOfferValue("");
} catch (err) {
setError(err instanceof Error ? err.message : "Erro ao processar compra");
} finally {
@@ -143,9 +205,9 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
switch (type) {
case "pix":
return "bg-green-500 hover:bg-green-600 text-white";
case "tc":
case "tibia_coins":
return "bg-blue-500 hover:bg-blue-600 text-white";
case "gp":
case "gold_coins":
return "bg-yellow-500 hover:bg-yellow-600 text-white";
case "trade":
return "bg-purple-500 hover:bg-purple-600 text-white";
@@ -173,6 +235,24 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
</p>
</div>
<div className="space-y-2">
<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">
{method.label}: {method.value}
</Badge>
))}
</div>
</div>
{highestOfferLabel && (
<div className="rounded-lg border border-primary/30 bg-primary/5 px-3 py-2 text-sm">
Maior oferta registrada:{" "}
<span className="font-semibold">{highestOfferLabel}</span>
</div>
)}
<Separator />
{/* Seleção de método de pagamento */}
@@ -224,6 +304,27 @@ export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: Purcha
</p>
)}
{selectedMethod &&
["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"})
</Label>
<Input
type="number"
min="0"
step={selectedMethod === "pix" ? "0.01" : "1"}
value={customOfferValue}
onChange={(event) => setCustomOfferValue(event.target.value)}
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.
</p>
</div>
)}
{error && (
<div className="bg-destructive/10 text-destructive p-3 rounded-lg text-sm">
{error}