place_maxlan/server/routes.ts
freesemar93 83fc03b313 Improve canvas zooming and scrolling with mouse wheel for smoother interaction
Implement mouse wheel zooming and smooth scrolling for the canvas component, enhance WebSocket connection management with automatic reconnection and keep-alive pings, and refine CSS for pixel hover effects.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 0385ea33-cde8-4bbd-8fce-8d192d30eb41
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/870d08ce-da3b-4822-9874-c2fe2b7628b1/0385ea33-cde8-4bbd-8fce-8d192d30eb41/PVrRiEe
2025-08-18 12:40:00 +00:00

170 lines
4.9 KiB
TypeScript

import type { Express } from "express";
import { createServer, type Server } from "http";
import { WebSocketServer, WebSocket } from "ws";
import { storage } from "./storage";
import { insertPixelSchema, insertUserCooldownSchema, type WSMessage } from "@shared/schema";
import { CanvasExporter } from "./export";
export async function registerRoutes(app: Express): Promise<Server> {
const httpServer = createServer(app);
const wss = new WebSocketServer({ server: httpServer, path: '/ws' });
let connectedUsers = new Set<WebSocket>();
// Initialize canvas exporter
const exporter = new CanvasExporter(storage);
exporter.startAutoExport();
// API Routes
app.get("/api/pixels", async (req, res) => {
try {
const pixels = await storage.getAllPixels();
res.json(pixels);
} catch (error) {
res.status(500).json({ message: "Failed to fetch pixels" });
}
});
app.get("/api/config", async (req, res) => {
try {
const config = await storage.getCanvasConfig();
res.json(config);
} catch (error) {
res.status(500).json({ message: "Failed to fetch config" });
}
});
// Config is now read-only from file
// Remove the POST endpoint for config updates
app.post("/api/pixels", async (req, res) => {
try {
const pixelData = insertPixelSchema.parse(req.body);
const config = await storage.getCanvasConfig();
// Validate coordinates
if (pixelData.x < 0 || pixelData.x >= config.canvasWidth ||
pixelData.y < 0 || pixelData.y >= config.canvasHeight) {
return res.status(400).json({ message: "Pixel coordinates out of bounds" });
}
// Check cooldown unless events are enabled
if (!config.enableAutomaticEvents) {
const cooldown = await storage.getUserCooldown(pixelData.userId);
if (cooldown && cooldown.cooldownEnds > new Date()) {
return res.status(429).json({ message: "Cooldown active" });
}
// Set new cooldown
const cooldownEnd = new Date(Date.now() + (config.defaultCooldown * 1000));
await storage.setUserCooldown({
userId: pixelData.userId,
cooldownEnds: cooldownEnd,
});
}
const pixel = await storage.placePixel(pixelData);
// Broadcast pixel placement to all connected clients
broadcast({
type: "pixel_placed",
data: {
x: pixel.x,
y: pixel.y,
color: pixel.color,
userId: pixel.userId,
username: pixel.username,
timestamp: pixel.createdAt.toISOString(),
},
});
res.json(pixel);
} catch (error) {
res.status(400).json({ message: "Invalid pixel data" });
}
});
app.get("/api/recent", async (req, res) => {
try {
const limit = parseInt(req.query.limit as string) || 10;
const recent = await storage.getRecentPlacements(limit);
res.json(recent);
} catch (error) {
res.status(500).json({ message: "Failed to fetch recent placements" });
}
});
app.get("/api/cooldown/:userId", async (req, res) => {
try {
const { userId } = req.params;
const cooldown = await storage.getUserCooldown(userId);
const config = await storage.getCanvasConfig();
if (!cooldown || config.enableAutomaticEvents) {
return res.json({ remainingSeconds: 0 });
}
const remaining = Math.max(0, Math.ceil((cooldown.cooldownEnds.getTime() - Date.now()) / 1000));
res.json({ remainingSeconds: remaining });
} catch (error) {
res.status(500).json({ message: "Failed to fetch cooldown" });
}
});
// WebSocket handling
function broadcast(message: WSMessage) {
const messageStr = JSON.stringify(message);
connectedUsers.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(messageStr);
}
});
}
function broadcastUserCount() {
broadcast({
type: "user_count",
data: { count: connectedUsers.size },
});
}
wss.on('connection', (ws, req) => {
console.log(`New WebSocket connection from ${req.socket.remoteAddress}`);
connectedUsers.add(ws);
broadcastUserCount();
// Send current user count immediately
ws.send(JSON.stringify({
type: "user_count",
data: { count: connectedUsers.size },
}));
ws.on('close', (code, reason) => {
console.log(`WebSocket disconnected: ${code} ${reason}`);
connectedUsers.delete(ws);
broadcastUserCount();
});
ws.on('error', (error) => {
console.error('WebSocket error:', error);
connectedUsers.delete(ws);
broadcastUserCount();
});
ws.on('pong', () => {
// Keep connection alive
});
});
// Keep connections alive with ping/pong
const pingInterval = setInterval(() => {
connectedUsers.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping();
}
});
}, 30000);
return httpServer;
}