Initial marketplace implementation
Full-stack marketplace for buying/selling second-hand items. React 19 + TypeScript + Tailwind CSS v4 frontend with 17 screens, Express + Prisma + Socket.io backend, Stripe payments, JWT auth. Deployed at https://marketplace.173.212.212.157.sslip.io/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
88
server/src/socket/index.ts
Normal file
88
server/src/socket/index.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { Server as HTTPServer } from 'http';
|
||||
import { Server } from 'socket.io';
|
||||
import { verifyAccessToken } from '../utils/jwt.js';
|
||||
import { prisma } from '../config/database.js';
|
||||
|
||||
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}`);
|
||||
|
||||
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 });
|
||||
}
|
||||
} 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('disconnect', () => {
|
||||
// Cleanup handled by Socket.io
|
||||
});
|
||||
});
|
||||
|
||||
return io;
|
||||
}
|
||||
Reference in New Issue
Block a user