Allow configuration through a file and export images automatically

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
This commit is contained in:
freesemar93
2025-08-18 12:20:13 +00:00
parent 7e739f216d
commit 7968b56976
10 changed files with 218 additions and 272 deletions

View File

@@ -57,10 +57,9 @@ export function Canvas({
gridTemplateRows: `repeat(${canvasHeight}, ${pixelSize}px)`,
width: `${canvasWidth * pixelSize}px`,
height: `${canvasHeight * pixelSize}px`,
backgroundSize: `${pixelSize}px ${pixelSize}px`,
};
const gridClass = showGrid ? "grid-lines" : "";
return (
<div className="flex-1 relative bg-canvas-bg overflow-hidden">
<div
@@ -69,7 +68,7 @@ export function Canvas({
data-testid="canvas-container"
>
<div
className={cn("grid mx-auto border border-gray-400 relative", gridClass)}
className={cn("grid mx-auto border border-gray-400 relative", showGrid && "grid-lines")}
style={canvasStyle}
data-testid="pixel-canvas"
>

View File

@@ -1,213 +0,0 @@
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Settings } from "lucide-react";
import { CanvasConfig, InsertCanvasConfig } from "@shared/schema";
import { useToast } from "@/hooks/use-toast";
interface ConfigModalProps {
config: CanvasConfig;
onConfigUpdate: (config: InsertCanvasConfig) => Promise<void>;
}
export function ConfigModal({ config, onConfigUpdate }: ConfigModalProps) {
const [open, setOpen] = useState(false);
const [formData, setFormData] = useState<InsertCanvasConfig>({
canvasWidth: config.canvasWidth,
canvasHeight: config.canvasHeight,
defaultCooldown: config.defaultCooldown,
enableAutomaticEvents: config.enableAutomaticEvents,
eventDuration: config.eventDuration,
eventInterval: config.eventInterval,
showGridByDefault: config.showGridByDefault,
});
const [isLoading, setIsLoading] = useState(false);
const { toast } = useToast();
useEffect(() => {
setFormData({
canvasWidth: config.canvasWidth,
canvasHeight: config.canvasHeight,
defaultCooldown: config.defaultCooldown,
enableAutomaticEvents: config.enableAutomaticEvents,
eventDuration: config.eventDuration,
eventInterval: config.eventInterval,
showGridByDefault: config.showGridByDefault,
});
}, [config]);
const handleSave = async () => {
setIsLoading(true);
try {
await onConfigUpdate(formData);
toast({
title: "Configuration saved",
description: "Canvas settings have been updated successfully.",
});
setOpen(false);
} catch (error) {
toast({
title: "Error",
description: "Failed to save configuration. Please try again.",
variant: "destructive",
});
} finally {
setIsLoading(false);
}
};
const handleReset = () => {
setFormData({
canvasWidth: 100,
canvasHeight: 100,
defaultCooldown: 5,
enableAutomaticEvents: false,
eventDuration: 30,
eventInterval: 6,
showGridByDefault: true,
});
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
className="flex items-center space-x-2 bg-primary hover:bg-red-500"
data-testid="button-config"
>
<Settings className="w-4 h-4" />
<span>Config</span>
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-2xl bg-panel-bg border-gray-700" data-testid="config-modal">
<DialogHeader>
<DialogTitle className="text-xl font-bold text-white">Admin Configuration</DialogTitle>
</DialogHeader>
<div className="space-y-6 py-4">
{/* Canvas Settings */}
<div>
<h3 className="text-lg font-semibold mb-3 text-white">Canvas Settings</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="canvasWidth" className="text-white">Canvas Width</Label>
<Input
id="canvasWidth"
type="number"
value={formData.canvasWidth}
onChange={(e) => setFormData({ ...formData, canvasWidth: parseInt(e.target.value) })}
className="bg-gray-700 border-gray-600 text-white"
data-testid="input-canvas-width"
/>
</div>
<div>
<Label htmlFor="canvasHeight" className="text-white">Canvas Height</Label>
<Input
id="canvasHeight"
type="number"
value={formData.canvasHeight}
onChange={(e) => setFormData({ ...formData, canvasHeight: parseInt(e.target.value) })}
className="bg-gray-700 border-gray-600 text-white"
data-testid="input-canvas-height"
/>
</div>
</div>
</div>
{/* Cooldown Settings */}
<div>
<h3 className="text-lg font-semibold mb-3 text-white">Cooldown Settings</h3>
<div className="space-y-4">
<div>
<Label htmlFor="defaultCooldown" className="text-white">Default Cooldown (seconds)</Label>
<Input
id="defaultCooldown"
type="number"
value={formData.defaultCooldown}
onChange={(e) => setFormData({ ...formData, defaultCooldown: parseInt(e.target.value) })}
className="bg-gray-700 border-gray-600 text-white"
data-testid="input-default-cooldown"
/>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="enableEvents"
checked={formData.enableAutomaticEvents}
onCheckedChange={(checked) => setFormData({ ...formData, enableAutomaticEvents: !!checked })}
data-testid="checkbox-enable-events"
/>
<Label htmlFor="enableEvents" className="text-white">Enable Automatic Events (No Cooldown Periods)</Label>
</div>
</div>
</div>
{/* Event Settings */}
<div>
<h3 className="text-lg font-semibold mb-3 text-white">Event Settings</h3>
<div className="space-y-4">
<div>
<Label htmlFor="eventDuration" className="text-white">Event Duration (minutes)</Label>
<Input
id="eventDuration"
type="number"
value={formData.eventDuration}
onChange={(e) => setFormData({ ...formData, eventDuration: parseInt(e.target.value) })}
className="bg-gray-700 border-gray-600 text-white"
data-testid="input-event-duration"
/>
</div>
<div>
<Label htmlFor="eventInterval" className="text-white">Event Interval (hours)</Label>
<Input
id="eventInterval"
type="number"
value={formData.eventInterval}
onChange={(e) => setFormData({ ...formData, eventInterval: parseInt(e.target.value) })}
className="bg-gray-700 border-gray-600 text-white"
data-testid="input-event-interval"
/>
</div>
</div>
</div>
{/* Grid Settings */}
<div>
<h3 className="text-lg font-semibold mb-3 text-white">Grid Settings</h3>
<div className="flex items-center space-x-2">
<Checkbox
id="defaultGrid"
checked={formData.showGridByDefault}
onCheckedChange={(checked) => setFormData({ ...formData, showGridByDefault: !!checked })}
data-testid="checkbox-default-grid"
/>
<Label htmlFor="defaultGrid" className="text-white">Show Grid by Default</Label>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex space-x-3 pt-6 border-t border-gray-700">
<Button
onClick={handleSave}
disabled={isLoading}
className="flex-1 bg-primary hover:bg-red-500 text-white"
data-testid="button-save-config"
>
{isLoading ? "Saving..." : "Save Configuration"}
</Button>
<Button
onClick={handleReset}
variant="secondary"
className="flex-1 bg-gray-600 hover:bg-gray-500 text-white"
data-testid="button-reset-config"
>
Reset to Defaults
</Button>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -125,8 +125,8 @@
.grid-lines {
background-image:
linear-gradient(to right, rgba(255,255,255,0.1) 1px, transparent 1px),
linear-gradient(to bottom, rgba(255,255,255,0.1) 1px, transparent 1px);
linear-gradient(to right, rgba(255,255,255,0.2) 1px, transparent 1px),
linear-gradient(to bottom, rgba(255,255,255,0.2) 1px, transparent 1px);
}
.cooldown-overlay {

View File

@@ -3,13 +3,13 @@ import { useQuery, useMutation } from "@tanstack/react-query";
import { queryClient } from "@/lib/queryClient";
import { Canvas } from "@/components/canvas";
import { ColorPalette } from "@/components/color-palette";
import { ConfigModal } from "@/components/config-modal";
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, InsertCanvasConfig, WSMessage } from "@shared/schema";
import { Pixel, CanvasConfig, InsertPixel, WSMessage } from "@shared/schema";
import { apiRequest } from "@/lib/queryClient";
export default function CanvasPage() {
@@ -48,14 +48,7 @@ export default function CanvasPage() {
});
break;
case "config_updated":
// Invalidate config cache
queryClient.invalidateQueries({ queryKey: ['/api/config'] });
toast({
title: "Configuration updated",
description: "Canvas settings have been changed by an administrator.",
});
break;
case "cooldown_update":
if (message.data.userId === userId) {
@@ -98,16 +91,14 @@ export default function CanvasPage() {
},
});
// Config update mutation
const updateConfigMutation = useMutation({
mutationFn: async (configUpdate: InsertCanvasConfig) => {
const response = await apiRequest("POST", "/api/config", configUpdate);
return response.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/config'] });
},
});
// Set initial grid state from config
useEffect(() => {
if (config) {
setShowGrid(config.showGridByDefault);
}
}, [config]);
// Cooldown countdown
useEffect(() => {
@@ -148,9 +139,7 @@ export default function CanvasPage() {
});
};
const handleConfigUpdate = async (configUpdate: InsertCanvasConfig) => {
await updateConfigMutation.mutateAsync(configUpdate);
};
if (pixelsLoading || configLoading || !config) {
return (
@@ -185,8 +174,7 @@ export default function CanvasPage() {
<span>Grid</span>
</Button>
{/* Admin Config Button */}
<ConfigModal config={config} onConfigUpdate={handleConfigUpdate} />
{/* User Info */}
<div className="flex items-center space-x-2 px-3 py-2 bg-panel-hover rounded-lg">