- 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
475 lines
14 KiB
TypeScript
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>
|
|
);
|
|
}
|