feat(market): add Minha Intenções tab with panel, hook, and badge

- Introduced "Minhas Intenções" tab visible only to logged-in users.
- Integrated MyPurchaseIntentionsPanel and useMyPurchaseIntentions hook.
- Added tab badge showing count of pending intentions.
- Extended session API to include user’s main character for downstream features.
This commit is contained in:
Evandro Fonseca Junior committed 2025-11-07 17:25:51 -03:00
1 parent f35fee944d
commit 7f42def478
55 files changed
+4906 -95

No files matched your search

+82
View File
@@ -0,0 +1,82 @@
/**
* Conexão MongoDB usando Mongoose
* Singleton pattern para reutilizar conexão
*/
import mongoose from "mongoose";
function getMongoUrl(): string {
const url = process.env.MONGO_URL;
if (!url) {
throw new Error("MONGO_URL não está definida nas variáveis de ambiente");
}
return url;
}
interface MongooseCache {
conn: typeof mongoose | null;
promise: Promise<typeof mongoose> | null;
}
// Cache global para Next.js (evita múltiplas conexões em hot reload)
declare global {
// eslint-disable-next-line no-var
var mongoose: MongooseCache | undefined;
}
let cached: MongooseCache = global.mongoose || { conn: null, promise: null };
if (!global.mongoose) {
global.mongoose = cached;
}
/**
* Conecta ao MongoDB ou retorna conexão existente
*/
export async function connectMongoDB(): Promise<typeof mongoose> {
if (cached.conn) {
return cached.conn;
}
if (!cached.promise) {
const opts = {
bufferCommands: false,
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
};
cached.promise = mongoose
.connect(getMongoUrl(), opts)
.then((mongoose) => {
// Conexão MongoDB estabelecida (log desabilitado)
return mongoose;
})
.catch((error) => {
console.error("❌ Erro ao conectar MongoDB:", error);
cached.promise = null;
throw error;
});
}
try {
cached.conn = await cached.promise;
} catch (e) {
cached.promise = null;
throw e;
}
return cached.conn;
}
/**
* Desconecta do MongoDB (útil para testes)
*/
export async function disconnectMongoDB(): Promise<void> {
if (cached.conn) {
await mongoose.disconnect();
cached.conn = null;
cached.promise = null;
}
}
+138
View File
@@ -0,0 +1,138 @@
/**
* Model Chat - Representa uma conversa entre comprador e vendedor
*/
import mongoose, { Schema, model, models } from "mongoose";
export interface IChat {
chat_id: string; // ID único: `${listing_id}_${buyer_id}_${seller_id}`
listing_id: number; // ID do anúncio (PostgreSQL)
buyer_id: number; // ID do comprador (PostgreSQL)
seller_id: number; // ID do vendedor (PostgreSQL)
created_at: Date;
updated_at: Date;
last_message_at: Date | null;
last_message_preview: string | null;
buyer_unread_count: number;
seller_unread_count: number;
is_active: boolean; // Chat ativo (anúncio ainda disponível)
// Dados cached do anúncio (para performance)
listing_cache: {
item_name: string;
item_image_url?: string;
price_pix?: number;
price_tibia_coins?: number;
price_gold_coins?: number;
server_name: string;
listing_status: string;
};
// Dados cached dos usuários
buyer_cache: {
user_id: number;
username: string;
character_name?: string;
};
seller_cache: {
user_id: number;
username: string;
character_name?: string;
};
}
const ChatSchema = new Schema<IChat>(
{
chat_id: {
type: String,
required: true,
unique: true,
index: true,
},
listing_id: {
type: Number,
required: true,
index: true,
},
buyer_id: {
type: Number,
required: true,
index: true,
},
seller_id: {
type: Number,
required: true,
index: true,
},
created_at: {
type: Date,
default: Date.now,
index: true,
},
updated_at: {
type: Date,
default: Date.now,
},
last_message_at: {
type: Date,
default: null,
index: true,
},
last_message_preview: {
type: String,
default: null,
maxlength: 100,
},
buyer_unread_count: {
type: Number,
default: 0,
},
seller_unread_count: {
type: Number,
default: 0,
},
is_active: {
type: Boolean,
default: true,
index: true,
},
listing_cache: {
item_name: { type: String, required: true },
item_image_url: { type: String, default: null },
price_pix: { type: Number, default: null },
price_tibia_coins: { type: Number, default: null },
price_gold_coins: { type: Number, default: null },
server_name: { type: String, required: true },
listing_status: { type: String, required: true },
},
buyer_cache: {
user_id: { type: Number, required: true },
username: { type: String, required: true },
character_name: { type: String, default: null },
},
seller_cache: {
user_id: { type: Number, required: true },
username: { type: String, required: true },
character_name: { type: String, default: null },
},
},
{
timestamps: false, // Usamos campos manuais
collection: "chats",
}
);
// Índices compostos para queries eficientes
ChatSchema.index({ buyer_id: 1, updated_at: -1 });
ChatSchema.index({ seller_id: 1, updated_at: -1 });
ChatSchema.index({ listing_id: 1, buyer_id: 1, seller_id: 1 });
// Atualizar updated_at antes de salvar
ChatSchema.pre("save", function (next) {
this.updated_at = new Date();
next();
});
export const Chat = models.Chat || model<IChat>("Chat", ChatSchema);
+81
View File
@@ -0,0 +1,81 @@
/**
* Model Message - Representa uma mensagem em um chat
*/
import mongoose, { Schema, model, models } from "mongoose";
export interface IMessage {
chat_id: string; // Referência ao chat
sender_id: number; // ID do remetente (PostgreSQL)
content: string; // Conteúdo da mensagem (sanitizado)
created_at: Date;
read_at: Date | null; // Quando foi lida
edited_at: Date | null; // Se foi editada
deleted_at: Date | null; // Soft delete
// Metadados
message_type: "text" | "system"; // System para mensagens automáticas
metadata?: {
listing_id?: number;
action?: "listing_updated" | "listing_sold" | "price_changed";
};
}
const MessageSchema = new Schema<IMessage>(
{
chat_id: {
type: String,
required: true,
index: true,
},
sender_id: {
type: Number,
required: true,
index: true,
},
content: {
type: String,
required: true,
maxlength: 1000,
trim: true,
},
created_at: {
type: Date,
default: Date.now,
index: true,
},
read_at: {
type: Date,
default: null,
},
edited_at: {
type: Date,
default: null,
},
deleted_at: {
type: Date,
default: null,
},
message_type: {
type: String,
enum: ["text", "system"],
default: "text",
},
metadata: {
type: Schema.Types.Mixed,
default: null,
},
},
{
timestamps: false, // Usamos campos manuais
collection: "messages",
}
);
// Índices compostos para queries eficientes
MessageSchema.index({ chat_id: 1, created_at: -1 }); // Para buscar mensagens de um chat ordenadas
MessageSchema.index({ chat_id: 1, sender_id: 1, created_at: -1 });
MessageSchema.index({ sender_id: 1, created_at: -1 });
export const Message = models.Message || model<IMessage>("Message", MessageSchema);