feat: implement market page with QueryProvider and dynamic listings
- wrap root layout with QueryProvider for React Query - replace market UnderConstruction placeholder with MarketClient and real data fetching - implement search, pagination, and sort via search params using getActiveListings - add necessary client components and queries - update dependencies (e.g., prisma, pkg-types)
This commit is contained in:
1 parent
55eebde0ba
commit
ab37b477df
106 files changed
+15261
-301
No files matched your search
@@ -0,0 +1,450 @@
|
||||
"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) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user