Introduce a configuration file for all settings, remove the web-based config editor, fix the grid display, and add automatic hourly PNG exports. 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/Zffw2vY
209 lines
6.4 KiB
TypeScript
209 lines
6.4 KiB
TypeScript
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<Pixel[]>({
|
|
queryKey: ['/api/pixels'],
|
|
});
|
|
|
|
const { data: config, isLoading: configLoading } = useQuery<CanvasConfig>({
|
|
queryKey: ['/api/config'],
|
|
});
|
|
|
|
const { data: recentPlacements = [] } = useQuery<Pixel[]>({
|
|
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 (
|
|
<div className="min-h-screen bg-canvas-bg text-white flex items-center justify-center">
|
|
<div className="text-lg">Loading canvas...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="h-screen flex flex-col bg-canvas-bg text-white">
|
|
{/* Header */}
|
|
<header className="bg-panel-bg border-b border-gray-700 px-4 py-3 flex items-center justify-between">
|
|
<div className="flex items-center space-x-4">
|
|
<h1 className="text-xl font-bold text-white">r/place Clone</h1>
|
|
<div className="flex items-center space-x-2 text-sm text-muted">
|
|
<div className={`w-2 h-2 rounded-full ${isConnected ? 'bg-success' : 'bg-red-500'}`} />
|
|
<span data-testid="user-count">{userCount}</span>
|
|
<span>users online</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-3">
|
|
{/* Grid Toggle */}
|
|
<Button
|
|
onClick={() => setShowGrid(!showGrid)}
|
|
className="flex items-center space-x-2 bg-panel-hover hover:bg-gray-600"
|
|
variant="secondary"
|
|
data-testid="button-grid-toggle"
|
|
>
|
|
<Grid className="w-4 h-4" />
|
|
<span>Grid</span>
|
|
</Button>
|
|
|
|
|
|
|
|
{/* User Info */}
|
|
<div className="flex items-center space-x-2 px-3 py-2 bg-panel-hover rounded-lg">
|
|
<div className="w-2 h-2 bg-success rounded-full" />
|
|
<span className="text-sm" data-testid="username">{username}</span>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Main Canvas Area */}
|
|
<div className="flex-1 flex overflow-hidden">
|
|
<Canvas
|
|
pixels={pixels}
|
|
selectedColor={selectedColor}
|
|
canvasWidth={config.canvasWidth}
|
|
canvasHeight={config.canvasHeight}
|
|
showGrid={showGrid}
|
|
onPixelClick={handlePixelClick}
|
|
cooldownActive={cooldownSeconds > 0}
|
|
/>
|
|
|
|
<ColorPalette
|
|
selectedColor={selectedColor}
|
|
onColorSelect={setSelectedColor}
|
|
cooldownSeconds={cooldownSeconds}
|
|
recentPlacements={recentPlacements}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|