import { useState, useEffect, useCallback } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { queryClient } from "@/lib/queryClient"; import { Canvas } from "@/components/canvas"; import { ColorPalette } from "@/components/color-palette"; import { Button } from "@/components/ui/button"; import { useWebSocket } from "@/hooks/use-websocket"; import { useToast } from "@/hooks/use-toast"; import { Grid } from "lucide-react"; import { DEFAULT_SELECTED_COLOR, generateUserId, getUsername } from "@/lib/config"; import { Pixel, CanvasConfig, InsertPixel, WSMessage } from "@shared/schema"; import { apiRequest } from "@/lib/queryClient"; export default function CanvasPage() { const [selectedColor, setSelectedColor] = useState(DEFAULT_SELECTED_COLOR); const [showGrid, setShowGrid] = useState(true); const [cooldownSeconds, setCooldownSeconds] = useState(0); const [userId] = useState(() => generateUserId()); const [username] = useState(() => getUsername()); const { toast } = useToast(); // Fetch initial data const { data: pixels = [], isLoading: pixelsLoading } = useQuery({ queryKey: ['/api/pixels'], }); const { data: config, isLoading: configLoading } = useQuery({ queryKey: ['/api/config'], }); const { data: recentPlacements = [] } = useQuery({ queryKey: ['/api/recent'], refetchInterval: 5000, // Refresh every 5 seconds }); // WebSocket handling const handleWebSocketMessage = useCallback((message: WSMessage) => { switch (message.type) { case "pixel_placed": // Invalidate pixels cache to refetch queryClient.invalidateQueries({ queryKey: ['/api/pixels'] }); queryClient.invalidateQueries({ queryKey: ['/api/recent'] }); toast({ title: "Pixel placed", description: `${message.data.username} placed a pixel at (${message.data.x}, ${message.data.y})`, }); break; case "cooldown_update": if (message.data.userId === userId) { setCooldownSeconds(message.data.remainingSeconds); } break; } }, [userId, toast]); const { isConnected, userCount } = useWebSocket(handleWebSocketMessage); // Pixel placement mutation const placePixelMutation = useMutation({ mutationFn: async (pixel: InsertPixel) => { const response = await apiRequest("POST", "/api/pixels", pixel); return response.json(); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/pixels'] }); queryClient.invalidateQueries({ queryKey: ['/api/recent'] }); // Start cooldown countdown if (config && !config.enableAutomaticEvents) { setCooldownSeconds(config.defaultCooldown); } }, onError: (error: any) => { if (error.message.includes("429")) { toast({ title: "Cooldown active", description: "Please wait before placing another pixel.", variant: "destructive", }); } else { toast({ title: "Error", description: "Failed to place pixel. Please try again.", variant: "destructive", }); } }, }); // Set initial grid state from config useEffect(() => { if (config) { setShowGrid(config.showGridByDefault); } }, [config]); // Cooldown countdown useEffect(() => { if (cooldownSeconds > 0) { const timer = setTimeout(() => { setCooldownSeconds(prev => Math.max(0, prev - 1)); }, 1000); return () => clearTimeout(timer); } }, [cooldownSeconds]); // Fetch initial cooldown state useEffect(() => { const fetchCooldown = async () => { try { const response = await fetch(`/api/cooldown/${userId}`); const data = await response.json(); setCooldownSeconds(data.remainingSeconds); } catch (error) { console.error("Failed to fetch cooldown:", error); } }; if (userId) { fetchCooldown(); } }, [userId]); const handlePixelClick = (x: number, y: number) => { if (cooldownSeconds > 0) return; placePixelMutation.mutate({ x, y, color: selectedColor, userId, username, }); }; if (pixelsLoading || configLoading || !config) { return (
Loading canvas...
); } return (
{/* Header */}

r/place Clone

{userCount} users online
{/* Grid Toggle */} {/* User Info */}
{username}
{/* Main Canvas Area */}
0} />
); }