Files
pagodera/components/marketplace/CreateAdModal.tsx
T
Evandro Fonseca Junior d8e7d5d7e6 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
2025-11-08 11:51:21 -03:00

475 lines
14 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
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,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useSession } from "@/lib/hooks/use-session";
import { ItemSearch } from "./ItemSearch";
import { ArrowRight, Package } from "lucide-react";
interface Item {
id: number;
name: string;
category?: string;
image?: string;
[key: string]: any;
}
interface CreateAdModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: () => void;
}
interface Server {
serverId: number;
serverName: string;
serverType: string;
location: string;
isActive: boolean;
}
export function CreateAdModal({ open, onOpenChange, onSuccess }: CreateAdModalProps) {
const { user } = useSession();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [servers, setServers] = useState<Server[]>([]);
const [loadingServers, setLoadingServers] = useState(false);
const [selectedItem, setSelectedItem] = useState<Item | null>(null);
const [tradeItem, setTradeItem] = useState<Item | null>(null);
const [acceptTrade, setAcceptTrade] = useState(false);
const [formData, setFormData] = useState({
quantity: "1",
server_name: "",
price_pix: "",
price_tibia_coins: "",
price_gold_coins: "",
trade_item_quantity: "1",
description: "",
});
const handleItemSelect = (item: Item | null) => {
setSelectedItem(item);
};
const handleTradeItemSelect = (item: Item | null) => {
setTradeItem(item);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
try {
// Validar item selecionado
if (!selectedItem) {
setError("Selecione um item para vender");
setLoading(false);
return;
}
// Validar pelo menos um método de pagamento (preço ou troca)
const hasPrice =
formData.price_pix ||
formData.price_tibia_coins ||
formData.price_gold_coins;
if (!hasPrice && (!acceptTrade || !tradeItem)) {
setError(
"Informe pelo menos um preço (PIX, TC ou GP) ou configure uma troca por item"
);
setLoading(false);
return;
}
// Validar troca se aceita troca
if (acceptTrade && !tradeItem) {
setError("Selecione o item que deseja receber em troca");
setLoading(false);
return;
}
const response = await fetch("/api/listings", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
item_id: selectedItem.id,
item_name: selectedItem.name,
quantity: parseInt(formData.quantity, 10),
server_name: formData.server_name,
price_pix: formData.price_pix ? parseFloat(formData.price_pix) : null,
price_tibia_coins: formData.price_tibia_coins
? parseInt(formData.price_tibia_coins, 10)
: null,
price_gold_coins: formData.price_gold_coins
? parseInt(formData.price_gold_coins, 10)
: null,
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,
}),
});
const data = await response.json();
if (!response.ok) {
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;
}
// Limpar formulário
setSelectedItem(null);
setTradeItem(null);
setAcceptTrade(false);
setFormData({
quantity: "1",
server_name: "",
price_pix: "",
price_tibia_coins: "",
price_gold_coins: "",
trade_item_quantity: "1",
description: "",
});
onOpenChange(false);
if (onSuccess) {
onSuccess();
}
} catch (err) {
setError("Erro ao criar anúncio. Tente novamente.");
console.error(err);
} finally {
setLoading(false);
}
};
// Carregar servidores ao abrir o modal
useEffect(() => {
if (open) {
loadServers();
}
}, [open]);
const loadServers = async () => {
setLoadingServers(true);
try {
const res = await fetch("/api/servers");
if (res.ok) {
const data = await res.json();
setServers(data.servers || []);
}
} catch (error) {
console.error("Erro ao carregar servidores:", error);
} finally {
setLoadingServers(false);
}
};
const handleClose = () => {
if (!loading) {
setSelectedItem(null);
setTradeItem(null);
setAcceptTrade(false);
setFormData({
quantity: "1",
server_name: "",
price_pix: "",
price_tibia_coins: "",
price_gold_coins: "",
trade_item_quantity: "1",
description: "",
});
setError(null);
onOpenChange(false);
}
};
if (!user) {
return null;
}
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Criar Novo Anúncio</DialogTitle>
<DialogDescription>
Preencha os dados do item que deseja vender
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Seleção do item a vender */}
<div className="space-y-2">
<Label htmlFor="item_search">Item a Vender *</Label>
<ItemSearch
onItemSelect={handleItemSelect}
selectedItem={selectedItem}
placeholder="Pesquise o nome do item..."
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="quantity">Quantidade *</Label>
<Input
id="quantity"
type="number"
min="1"
required
value={formData.quantity}
onChange={(e) =>
setFormData({ ...formData, quantity: e.target.value })
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="server_name">Servidor *</Label>
<Select
value={formData.server_name}
onValueChange={(value) =>
setFormData({ ...formData, server_name: value })
}
required
disabled={loadingServers}
>
<SelectTrigger id="server_name">
<SelectValue placeholder="Selecione um servidor" />
</SelectTrigger>
<SelectContent>
{servers.length === 0 && !loadingServers ? (
<SelectItem value="" disabled>
Nenhum servidor disponível
</SelectItem>
) : (
servers.map((server) => (
<SelectItem key={server.serverId} value={server.serverName}>
{server.serverName}
{server.location && ` (${server.location})`}
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
</div>
<Separator />
{/* Preços */}
<div className="space-y-2">
<Label>Preços (opcional se aceitar troca)</Label>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="price_pix" className="text-sm">
PIX (R$)
</Label>
<Input
id="price_pix"
type="number"
step="0.01"
min="0"
value={formData.price_pix}
onChange={(e) =>
setFormData({ ...formData, price_pix: e.target.value })
}
placeholder="0.00"
/>
</div>
<div className="space-y-2">
<Label htmlFor="price_tibia_coins" className="text-sm">
Tibia Coins
</Label>
<Input
id="price_tibia_coins"
type="number"
min="0"
value={formData.price_tibia_coins}
onChange={(e) =>
setFormData({
...formData,
price_tibia_coins: e.target.value,
})
}
placeholder="0"
/>
</div>
<div className="space-y-2">
<Label htmlFor="price_gold_coins" className="text-sm">
Gold Coins
</Label>
<Input
id="price_gold_coins"
type="number"
min="0"
value={formData.price_gold_coins}
onChange={(e) =>
setFormData({
...formData,
price_gold_coins: e.target.value,
})
}
placeholder="0"
/>
</div>
</div>
</div>
<Separator />
{/* Troca por item */}
<div className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="accept_trade"
checked={acceptTrade}
onCheckedChange={(checked) => {
setAcceptTrade(checked as boolean);
if (!checked) {
setTradeItem(null);
}
}}
/>
<Label
htmlFor="accept_trade"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer flex items-center gap-2"
>
<Package className="h-4 w-4" />
Aceitar troca por item (além dos valores monetários)
</Label>
</div>
{acceptTrade && (
<div className="space-y-4 pl-6 border-l-2 border-primary/20">
<div className="space-y-2">
<Label>Item Desejado em Troca *</Label>
<ItemSearch
onItemSelect={handleTradeItemSelect}
selectedItem={tradeItem}
placeholder="Pesquise o item que deseja receber..."
/>
</div>
{tradeItem && (
<div className="space-y-2">
<Label htmlFor="trade_item_quantity">
Quantidade do Item de Troca *
</Label>
<Input
id="trade_item_quantity"
type="number"
min="1"
required
value={formData.trade_item_quantity}
onChange={(e) =>
setFormData({
...formData,
trade_item_quantity: e.target.value,
})
}
/>
</div>
)}
</div>
)}
</div>
<Separator />
{/* Descrição */}
<div className="space-y-2">
<Label htmlFor="description">Descrição (opcional)</Label>
<Textarea
id="description"
maxLength={5000}
value={formData.description}
onChange={(e) =>
setFormData({ ...formData, description: e.target.value })
}
placeholder="Descreva o item, condições, etc..."
rows={4}
/>
<p className="text-xs text-muted-foreground">
{formData.description.length}/5000 caracteres
</p>
</div>
{error && (
<div className="p-3 bg-destructive/10 text-destructive rounded-md text-sm">
{error}
</div>
)}
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
onClick={handleClose}
disabled={loading}
>
Cancelar
</Button>
<Button type="submit" disabled={loading}>
{loading ? "Criando..." : "Criar Anúncio"}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}