feat: Implement multi-image product display with fading and hover effects, and introduce lazy-loaded HTML sanitization.

This commit is contained in:
sebseb7
2026-03-10 11:27:15 +01:00
parent fb6c1159fe
commit 65a676de46
6 changed files with 403 additions and 205 deletions

View File

@@ -1,12 +1,9 @@
import { io } from 'socket.io-client';
class SocketManager {
constructor() {
this.socket = io('', {
transports: ["websocket", "polling"],
autoConnect: false
});
this._socket = null;
this._socketReady = null;
// Listeners registered before socket.io-client has loaded
this._preSocketListeners = [];
this.emit = this.emit.bind(this);
this.on = this.on.bind(this);
@@ -14,30 +11,57 @@ class SocketManager {
this.connectPromise = null;
this.pendingListeners = new Map();
}
on(event, callback) {
// If socket is already connected, register the listener directly
if (this.socket.connected) {
this.socket.on(event, callback);
return;
}
// Store the listener to be registered when connection is established
if (!this.pendingListeners.has(event)) {
this.pendingListeners.set(event, new Set());
}
this.pendingListeners.get(event).add(callback);
// Register the listener now, it will receive events once connected
this.socket.on(event, callback);
// Lazily import socket.io-client and create the socket on first use.
// Subsequent calls return the same promise.
_ensureSocket() {
if (this._socket) return Promise.resolve(this._socket);
if (this._socketReady) return this._socketReady;
this._socketReady = import('socket.io-client').then(({ io }) => {
this._socket = io('', {
transports: ['websocket', 'polling'],
autoConnect: false,
});
// Register any listeners that arrived before the socket was ready
this._preSocketListeners.forEach(({ event, callback }) => {
this._socket.on(event, callback);
});
this._preSocketListeners = [];
return this._socket;
});
return this._socketReady;
}
on(event, callback) {
if (this._socket) {
// Socket already loaded — mirror the original behaviour
if (!this.pendingListeners.has(event)) {
this.pendingListeners.set(event, new Set());
}
this.pendingListeners.get(event).add(callback);
this._socket.on(event, callback);
} else {
// Queue for when socket.io-client finishes loading
this._preSocketListeners.push({ event, callback });
if (!this.pendingListeners.has(event)) {
this.pendingListeners.set(event, new Set());
}
this.pendingListeners.get(event).add(callback);
}
}
off(event, callback) {
// Remove from socket listeners
console.log('off', event, callback);
this.socket.off(event, callback);
// Remove from pending listeners if present
// Remove from pre-socket queue (component unmounted before socket loaded)
this._preSocketListeners = this._preSocketListeners.filter(
(item) => !(item.event === event && item.callback === callback)
);
if (this.pendingListeners.has(event)) {
const listeners = this.pendingListeners.get(event);
listeners.delete(callback);
@@ -45,57 +69,60 @@ class SocketManager {
this.pendingListeners.delete(event);
}
}
if (this._socket) {
this._socket.off(event, callback);
}
}
connect() {
if (this.connectPromise) return this.connectPromise;
this.connectPromise = new Promise((resolve, reject) => {
this.socket.connect();
this.socket.once('connect', () => {
resolve();
});
this.socket.once('connect_error', (error) => {
this.connectPromise = null;
reject(error);
});
});
return this.connectPromise;
}
emit(event, ...args) {
return new Promise((resolve, reject) => {
if (!this.socket.connected) {
// If not already connecting, start connection
if (!this.connectPromise) {
this.connect();
}
// Wait for connection before emitting
this.connectPromise
.then(() => {
this.socket.emit(event, ...args);
this.connectPromise = this._ensureSocket().then(
(socket) =>
new Promise((resolve, reject) => {
socket.connect();
socket.once('connect', () => {
resolve();
})
.catch((error) => {
});
socket.once('connect_error', (error) => {
this.connectPromise = null;
reject(error);
});
} else {
// Socket already connected, emit directly
this.socket.emit(event, ...args);
resolve();
}
})
);
return this.connectPromise;
}
emit(event, ...args) {
return new Promise((resolve, reject) => {
this._ensureSocket()
.then((socket) => {
if (!socket.connected) {
if (!this.connectPromise) {
this.connect();
}
this.connectPromise
.then(() => {
socket.emit(event, ...args);
resolve();
})
.catch((error) => {
reject(error);
});
} else {
socket.emit(event, ...args);
resolve();
}
})
.catch(reject);
});
}
}
// Create singleton instance
// Create singleton instance and expose globally so all components can reach it
const socketManager = new SocketManager();
// Attach to window object
window.socketManager = socketManager;
export default socketManager;
export default socketManager;