- SellItemPage: real file upload + API listing creation + activate - CreateProfilePage: save profile via PUT /users/profile - ProductDetailPage: wire edit/delete/message buttons, show edit for owner - ListingCard: persist favorites via API, show real images - Footer: connect newsletter subscribe to API - Router: add /dashboard/listings and /dashboard/saved routes - Backend: add GET /listings/favorites endpoint - New pages: MyListingsPage, SavedItemsPage - Fix unused imports causing build failures Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
126 lines
4.1 KiB
TypeScript
126 lines
4.1 KiB
TypeScript
import type { Server as HTTPServer } from 'http';
|
|
import { Server } from 'socket.io';
|
|
import { verifyAccessToken } from '../utils/jwt.js';
|
|
import { prisma } from '../config/database.js';
|
|
|
|
// Online status tracking
|
|
const onlineUsers = new Map<string, Set<string>>();
|
|
|
|
export function setupSocket(httpServer: HTTPServer) {
|
|
const io = new Server(httpServer, {
|
|
cors: {
|
|
origin: process.env['CLIENT_URL'] || 'http://localhost:5173',
|
|
credentials: true,
|
|
},
|
|
});
|
|
|
|
io.use((socket, next) => {
|
|
const token = socket.handshake.auth.token;
|
|
if (!token) return next(new Error('Authentication required'));
|
|
try {
|
|
const payload = verifyAccessToken(token);
|
|
socket.data.userId = payload.userId;
|
|
next();
|
|
} catch {
|
|
next(new Error('Invalid token'));
|
|
}
|
|
});
|
|
|
|
io.on('connection', (socket) => {
|
|
const userId = socket.data.userId;
|
|
socket.join(`user:${userId}`);
|
|
|
|
// Track online status
|
|
if (!onlineUsers.has(userId)) {
|
|
onlineUsers.set(userId, new Set());
|
|
}
|
|
onlineUsers.get(userId)!.add(socket.id);
|
|
|
|
// Broadcast online status
|
|
socket.broadcast.emit('user_online', { userId });
|
|
|
|
socket.on('join_conversation', (conversationId: string) => {
|
|
socket.join(`conversation:${conversationId}`);
|
|
});
|
|
|
|
socket.on('leave_conversation', (conversationId: string) => {
|
|
socket.leave(`conversation:${conversationId}`);
|
|
});
|
|
|
|
socket.on('send_message', async (data: { conversationId: string; content: string; offerAmount?: number }) => {
|
|
try {
|
|
const message = await prisma.message.create({
|
|
data: {
|
|
content: data.content,
|
|
senderId: userId,
|
|
conversationId: data.conversationId,
|
|
offerAmount: data.offerAmount,
|
|
},
|
|
include: { sender: { select: { id: true, fullName: true, avatar: true } } },
|
|
});
|
|
|
|
await prisma.conversation.update({
|
|
where: { id: data.conversationId },
|
|
data: { updatedAt: new Date() },
|
|
});
|
|
|
|
io.to(`conversation:${data.conversationId}`).emit('new_message', message);
|
|
|
|
const conversation = await prisma.conversation.findUnique({ where: { id: data.conversationId } });
|
|
if (conversation) {
|
|
const recipientId = conversation.user1Id === userId ? conversation.user2Id : conversation.user1Id;
|
|
io.to(`user:${recipientId}`).emit('message_notification', { conversationId: data.conversationId, message });
|
|
|
|
// Create notification
|
|
const sender = await prisma.user.findUnique({ where: { id: userId }, select: { fullName: true } });
|
|
const notification = await prisma.notification.create({
|
|
data: {
|
|
userId: recipientId,
|
|
type: 'NEW_MESSAGE',
|
|
title: 'New Message',
|
|
body: `${sender?.fullName || 'Someone'} sent you a message`,
|
|
data: { conversationId: data.conversationId },
|
|
},
|
|
});
|
|
io.to(`user:${recipientId}`).emit('new_notification', notification);
|
|
}
|
|
} catch (error) {
|
|
socket.emit('error', { message: 'Failed to send message' });
|
|
}
|
|
});
|
|
|
|
socket.on('typing', (conversationId: string) => {
|
|
socket.to(`conversation:${conversationId}`).emit('user_typing', { userId, conversationId });
|
|
});
|
|
|
|
socket.on('stop_typing', (conversationId: string) => {
|
|
socket.to(`conversation:${conversationId}`).emit('user_stop_typing', { userId, conversationId });
|
|
});
|
|
|
|
socket.on('mark_read', async (conversationId: string) => {
|
|
await prisma.message.updateMany({
|
|
where: { conversationId, senderId: { not: userId }, isRead: false },
|
|
data: { isRead: true },
|
|
});
|
|
});
|
|
|
|
socket.on('get_online_users', () => {
|
|
const users = Array.from(onlineUsers.keys());
|
|
socket.emit('online_users', users);
|
|
});
|
|
|
|
socket.on('disconnect', () => {
|
|
const userSockets = onlineUsers.get(userId);
|
|
if (userSockets) {
|
|
userSockets.delete(socket.id);
|
|
if (userSockets.size === 0) {
|
|
onlineUsers.delete(userId);
|
|
socket.broadcast.emit('user_offline', { userId });
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
return io;
|
|
}
|