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,263 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
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";
|
||||
|
||||
interface Listing {
|
||||
listing_id: number;
|
||||
item_name: string;
|
||||
quantity: number;
|
||||
price_pix: number | string | null;
|
||||
price_tibia_coins: number | string | null;
|
||||
price_gold_coins: number | string | null;
|
||||
trade_item_id?: number | null;
|
||||
trade_item_name?: string | null;
|
||||
trade_item_quantity?: number | null;
|
||||
}
|
||||
|
||||
interface PurchaseModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
listing: Listing | null;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
export function PurchaseModal({ open, onOpenChange, listing, onSuccess }: PurchaseModalProps) {
|
||||
const { user } = useSession();
|
||||
const [selectedMethod, setSelectedMethod] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
if (!listing) return null;
|
||||
|
||||
// Determinar métodos de pagamento disponíveis
|
||||
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)) {
|
||||
availableMethods.push({
|
||||
type: "pix",
|
||||
label: "PIX",
|
||||
value: `R$ ${price.toFixed(2)}`,
|
||||
icon: DollarSign,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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)) {
|
||||
availableMethods.push({
|
||||
type: "tc",
|
||||
label: "Tibia Coins",
|
||||
value: `${price.toLocaleString()} TC`,
|
||||
icon: Coins,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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)) {
|
||||
availableMethods.push({
|
||||
type: "gp",
|
||||
label: "Gold Coins",
|
||||
value: `${price.toLocaleString()} GP`,
|
||||
icon: Coins,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (listing.trade_item_id && listing.trade_item_name && listing.trade_item_quantity) {
|
||||
availableMethods.push({
|
||||
type: "trade",
|
||||
label: "Troca",
|
||||
value: `Troca por ${listing.trade_item_quantity}x ${listing.trade_item_name}`,
|
||||
icon: RefreshCw,
|
||||
});
|
||||
}
|
||||
|
||||
// Se houver apenas um método, selecionar automaticamente
|
||||
if (availableMethods.length === 1 && !selectedMethod) {
|
||||
setSelectedMethod(availableMethods[0].type);
|
||||
}
|
||||
|
||||
const handlePurchase = async () => {
|
||||
if (!selectedMethod) {
|
||||
setError("Selecione um método de pagamento");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/listings/${listing.listing_id}/purchase`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
payment_method: selectedMethod,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.error || "Erro ao processar compra");
|
||||
}
|
||||
|
||||
// Sucesso
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
onOpenChange(false);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Erro ao processar compra");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getMethodClassName = (type: string) => {
|
||||
switch (type) {
|
||||
case "pix":
|
||||
return "bg-green-500 hover:bg-green-600 text-white";
|
||||
case "tc":
|
||||
return "bg-blue-500 hover:bg-blue-600 text-white";
|
||||
case "gp":
|
||||
return "bg-yellow-500 hover:bg-yellow-600 text-white";
|
||||
case "trade":
|
||||
return "bg-purple-500 hover:bg-purple-600 text-white";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Confirmar Compra</DialogTitle>
|
||||
<DialogDescription>
|
||||
Escolha o método de pagamento para finalizar a compra
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Resumo do item */}
|
||||
<div className="bg-muted p-4 rounded-lg">
|
||||
<h3 className="font-semibold mb-2">{listing.item_name}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Quantidade: {listing.quantity}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Seleção de método de pagamento */}
|
||||
{availableMethods.length > 1 ? (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Método de Pagamento</Label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{availableMethods.map((method) => {
|
||||
const Icon = method.icon;
|
||||
const isSelected = selectedMethod === method.type;
|
||||
return (
|
||||
<button
|
||||
key={method.type}
|
||||
type="button"
|
||||
onClick={() => setSelectedMethod(method.type)}
|
||||
className={`p-4 rounded-lg border-2 transition-all text-left ${
|
||||
isSelected
|
||||
? `${getMethodClassName(method.type)} border-primary`
|
||||
: "border-muted hover:border-primary/50"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Icon className="h-4 w-4" />
|
||||
<span className="font-medium">{method.label}</span>
|
||||
</div>
|
||||
<p className="text-sm opacity-90">{method.value}</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : 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="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>
|
||||
</div>
|
||||
<p className="text-sm opacity-90">{availableMethods[0].value}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Nenhum método de pagamento disponível
|
||||
</p>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="bg-destructive/10 text-destructive p-3 rounded-lg text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Botões */}
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={loading}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handlePurchase}
|
||||
disabled={loading || !selectedMethod || availableMethods.length === 0}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Processando...
|
||||
</>
|
||||
) : (
|
||||
"Confirmar Compra"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user