Add login and register dialogs, user dropdown menu with profile link and logout option, and integrate with session management hook. Prevent hydration mismatches by conditionally rendering auth elements after mount. Update package-lock.json with new @esbuild dependencies for build tooling.
339 lines
7.6 KiB
TypeScript
339 lines
7.6 KiB
TypeScript
/**
|
|
* Queries de listings (anúncios)
|
|
* CRUD completo para a tabela listings
|
|
*/
|
|
|
|
import { query } from "../index";
|
|
|
|
export interface Listing {
|
|
listing_id: number;
|
|
seller_id: number;
|
|
item_id: number;
|
|
item_name: string;
|
|
quantity: number;
|
|
server_name: string;
|
|
price_pix: number | null;
|
|
price_tibia_coins: number | null;
|
|
price_gold_coins: number | null;
|
|
is_auction: boolean;
|
|
current_bid_amount: number | null;
|
|
current_bidder_id: number | null;
|
|
auction_ends_at: Date | null;
|
|
description: string | null;
|
|
listing_status: "active" | "sold" | "cancelled" | "in_escrow" | "expired";
|
|
created_at: Date;
|
|
updated_at: Date;
|
|
sold_at: Date | null;
|
|
views_count: number;
|
|
}
|
|
|
|
export interface CreateListingData {
|
|
seller_id: number;
|
|
item_id: number;
|
|
item_name: string;
|
|
quantity: number;
|
|
server_name: string;
|
|
price_pix?: number;
|
|
price_tibia_coins?: number;
|
|
price_gold_coins?: number;
|
|
is_auction?: boolean;
|
|
auction_ends_at?: Date;
|
|
description?: string;
|
|
}
|
|
|
|
export interface UpdateListingData {
|
|
quantity?: number;
|
|
price_pix?: number;
|
|
price_tibia_coins?: number;
|
|
price_gold_coins?: number;
|
|
description?: string;
|
|
listing_status?: Listing["listing_status"];
|
|
current_bid_amount?: number;
|
|
current_bidder_id?: number;
|
|
}
|
|
|
|
export interface ListingFilters {
|
|
status?: Listing["listing_status"];
|
|
server_name?: string;
|
|
item_id?: number;
|
|
seller_id?: number;
|
|
is_auction?: boolean;
|
|
search?: string;
|
|
page?: number;
|
|
pageSize?: number;
|
|
sortBy?: "created_at" | "price_pix" | "price_tibia_coins" | "price_gold_coins" | "views_count";
|
|
sortOrder?: "ASC" | "DESC";
|
|
}
|
|
|
|
/**
|
|
* Cria um novo anúncio
|
|
*/
|
|
export async function createListing(
|
|
data: CreateListingData
|
|
): Promise<Listing> {
|
|
const result = await query<Listing>(
|
|
`
|
|
INSERT INTO listings (
|
|
seller_id, item_id, item_name, quantity, server_name,
|
|
price_pix, price_tibia_coins, price_gold_coins,
|
|
is_auction, auction_ends_at, description
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
RETURNING *
|
|
`,
|
|
[
|
|
data.seller_id,
|
|
data.item_id,
|
|
data.item_name,
|
|
data.quantity,
|
|
data.server_name,
|
|
data.price_pix || null,
|
|
data.price_tibia_coins || null,
|
|
data.price_gold_coins || null,
|
|
data.is_auction || false,
|
|
data.auction_ends_at || null,
|
|
data.description || null,
|
|
]
|
|
);
|
|
|
|
return result.rows[0];
|
|
}
|
|
|
|
/**
|
|
* Busca anúncio por ID
|
|
*/
|
|
export async function getListingById(listingId: number): Promise<Listing | null> {
|
|
const result = await query<Listing>(
|
|
`
|
|
SELECT * FROM listings WHERE listing_id = $1
|
|
`,
|
|
[listingId]
|
|
);
|
|
|
|
return result.rows[0] || null;
|
|
}
|
|
|
|
/**
|
|
* Lista anúncios com filtros e paginação
|
|
*/
|
|
export async function getListings(
|
|
filters: ListingFilters = {}
|
|
): Promise<{ listings: Listing[]; total: number }> {
|
|
const {
|
|
status,
|
|
server_name,
|
|
item_id,
|
|
seller_id,
|
|
is_auction,
|
|
search,
|
|
page = 1,
|
|
pageSize = 20,
|
|
sortBy = "created_at",
|
|
sortOrder = "DESC",
|
|
} = filters;
|
|
|
|
const conditions: string[] = [];
|
|
const params: any[] = [];
|
|
let paramIndex = 1;
|
|
|
|
if (status) {
|
|
conditions.push(`listing_status = $${paramIndex}`);
|
|
params.push(status);
|
|
paramIndex++;
|
|
}
|
|
|
|
if (server_name) {
|
|
conditions.push(`server_name = $${paramIndex}`);
|
|
params.push(server_name);
|
|
paramIndex++;
|
|
}
|
|
|
|
if (item_id) {
|
|
conditions.push(`item_id = $${paramIndex}`);
|
|
params.push(item_id);
|
|
paramIndex++;
|
|
}
|
|
|
|
if (seller_id) {
|
|
conditions.push(`seller_id = $${paramIndex}`);
|
|
params.push(seller_id);
|
|
paramIndex++;
|
|
}
|
|
|
|
if (is_auction !== undefined) {
|
|
conditions.push(`is_auction = $${paramIndex}`);
|
|
params.push(is_auction);
|
|
paramIndex++;
|
|
}
|
|
|
|
if (search) {
|
|
conditions.push(`(item_name ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`);
|
|
params.push(`%${search}%`);
|
|
paramIndex++;
|
|
}
|
|
|
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
|
|
// Contar total
|
|
const countResult = await query<{ count: string }>(
|
|
`SELECT COUNT(*) as count FROM listings ${whereClause}`,
|
|
params
|
|
);
|
|
const total = parseInt(countResult.rows[0].count, 10);
|
|
|
|
// Validar sortBy
|
|
const validSortColumns = ["created_at", "price_pix", "price_tibia_coins", "price_gold_coins", "views_count"];
|
|
const sortColumn = validSortColumns.includes(sortBy) ? sortBy : "created_at";
|
|
const sortDirection = sortOrder === "ASC" ? "ASC" : "DESC";
|
|
|
|
// Buscar listings
|
|
const offset = (page - 1) * pageSize;
|
|
const result = await query<Listing>(
|
|
`
|
|
SELECT * FROM listings
|
|
${whereClause}
|
|
ORDER BY ${sortColumn} ${sortDirection}
|
|
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
|
`,
|
|
[...params, pageSize, offset]
|
|
);
|
|
|
|
return {
|
|
listings: result.rows,
|
|
total,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Atualiza um anúncio
|
|
*/
|
|
export async function updateListing(
|
|
listingId: number,
|
|
data: UpdateListingData
|
|
): Promise<Listing | null> {
|
|
const updates: string[] = [];
|
|
const params: any[] = [];
|
|
let paramIndex = 1;
|
|
|
|
if (data.quantity !== undefined) {
|
|
updates.push(`quantity = $${paramIndex}`);
|
|
params.push(data.quantity);
|
|
paramIndex++;
|
|
}
|
|
|
|
if (data.price_pix !== undefined) {
|
|
updates.push(`price_pix = $${paramIndex}`);
|
|
params.push(data.price_pix);
|
|
paramIndex++;
|
|
}
|
|
|
|
if (data.price_tibia_coins !== undefined) {
|
|
updates.push(`price_tibia_coins = $${paramIndex}`);
|
|
params.push(data.price_tibia_coins);
|
|
paramIndex++;
|
|
}
|
|
|
|
if (data.price_gold_coins !== undefined) {
|
|
updates.push(`price_gold_coins = $${paramIndex}`);
|
|
params.push(data.price_gold_coins);
|
|
paramIndex++;
|
|
}
|
|
|
|
if (data.description !== undefined) {
|
|
updates.push(`description = $${paramIndex}`);
|
|
params.push(data.description);
|
|
paramIndex++;
|
|
}
|
|
|
|
if (data.listing_status !== undefined) {
|
|
updates.push(`listing_status = $${paramIndex}`);
|
|
params.push(data.listing_status);
|
|
paramIndex++;
|
|
}
|
|
|
|
if (data.current_bid_amount !== undefined) {
|
|
updates.push(`current_bid_amount = $${paramIndex}`);
|
|
params.push(data.current_bid_amount);
|
|
paramIndex++;
|
|
}
|
|
|
|
if (data.current_bidder_id !== undefined) {
|
|
updates.push(`current_bidder_id = $${paramIndex}`);
|
|
params.push(data.current_bidder_id);
|
|
paramIndex++;
|
|
}
|
|
|
|
if (updates.length === 0) {
|
|
return getListingById(listingId);
|
|
}
|
|
|
|
params.push(listingId);
|
|
const result = await query<Listing>(
|
|
`
|
|
UPDATE listings
|
|
SET ${updates.join(", ")}
|
|
WHERE listing_id = $${paramIndex}
|
|
RETURNING *
|
|
`,
|
|
params
|
|
);
|
|
|
|
return result.rows[0] || null;
|
|
}
|
|
|
|
/**
|
|
* Cancela um anúncio (soft delete - muda status)
|
|
*/
|
|
export async function deleteListing(listingId: number): Promise<boolean> {
|
|
const result = await query(
|
|
`
|
|
UPDATE listings
|
|
SET listing_status = 'cancelled'
|
|
WHERE listing_id = $1
|
|
`,
|
|
[listingId]
|
|
);
|
|
|
|
return result.rowCount > 0;
|
|
}
|
|
|
|
/**
|
|
* Incrementa contador de views
|
|
*/
|
|
export async function incrementViews(listingId: number): Promise<void> {
|
|
await query(
|
|
`
|
|
UPDATE listings
|
|
SET views_count = views_count + 1
|
|
WHERE listing_id = $1
|
|
`,
|
|
[listingId]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Busca anúncios de um vendedor
|
|
*/
|
|
export async function getListingsBySeller(
|
|
sellerId: number,
|
|
filters: { status?: Listing["listing_status"]; page?: number; pageSize?: number } = {}
|
|
): Promise<{ listings: Listing[]; total: number }> {
|
|
return getListings({
|
|
seller_id: sellerId,
|
|
...filters,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Busca apenas anúncios ativos
|
|
*/
|
|
export async function getActiveListings(
|
|
filters: Omit<ListingFilters, "status"> = {}
|
|
): Promise<{ listings: Listing[]; total: number }> {
|
|
return getListings({
|
|
...filters,
|
|
status: "active",
|
|
});
|
|
}
|
|
|