coldown fix
This commit is contained in:
@@ -4,12 +4,14 @@ import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import CanvasPage from "@/pages/canvas";
|
||||
import AdminPage from "@/pages/admin";
|
||||
import NotFound from "@/pages/not-found";
|
||||
|
||||
function Router() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path="/" component={CanvasPage} />
|
||||
<Route path="/admin" component={AdminPage} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
12
client/src/lib/canvas-api.ts
Normal file
12
client/src/lib/canvas-api.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
import { apiRequest } from "./queryClient";
|
||||
|
||||
export interface ExpandCanvasRequest {
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
}
|
||||
|
||||
export const expandCanvas = async (dimensions: ExpandCanvasRequest) => {
|
||||
const response = await apiRequest("POST", "/api/config/expand", dimensions);
|
||||
return response.json();
|
||||
};
|
||||
382
client/src/pages/admin.tsx
Normal file
382
client/src/pages/admin.tsx
Normal file
@@ -0,0 +1,382 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { queryClient } from "@/lib/queryClient";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Pixel, CanvasConfig } from "@shared/schema";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { AuthBanner } from "@/components/auth-banner";
|
||||
|
||||
export default function AdminPage() {
|
||||
const [adminKey, setAdminKey] = useState("");
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [selectedPixelId, setSelectedPixelId] = useState("");
|
||||
const [canvasWidth, setCanvasWidth] = useState(500);
|
||||
const [canvasHeight, setCanvasHeight] = useState(200);
|
||||
const { toast } = useToast();
|
||||
|
||||
// Auth check
|
||||
const checkAuth = async (key: string) => {
|
||||
try {
|
||||
const response = await apiRequest("POST", "/api/admin/auth", { adminKey: key });
|
||||
if (response.ok) {
|
||||
setIsAuthenticated(true);
|
||||
localStorage.setItem("adminKey", key);
|
||||
toast({ title: "Admin-Zugang gewährt" });
|
||||
} else {
|
||||
throw new Error("Invalid key");
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Ungültiger Admin-Schlüssel",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Check saved auth on load
|
||||
useEffect(() => {
|
||||
const savedKey = localStorage.getItem("adminKey");
|
||||
if (savedKey) {
|
||||
setAdminKey(savedKey);
|
||||
checkAuth(savedKey);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Fetch data
|
||||
const { data: pixels = [], refetch: refetchPixels } = useQuery<Pixel[]>({
|
||||
queryKey: ['/api/pixels'],
|
||||
enabled: isAuthenticated,
|
||||
});
|
||||
|
||||
const { data: config } = useQuery<CanvasConfig>({
|
||||
queryKey: ['/api/config'],
|
||||
enabled: isAuthenticated,
|
||||
});
|
||||
|
||||
const { data: adminStats } = useQuery({
|
||||
queryKey: ['/api/admin/stats'],
|
||||
enabled: isAuthenticated,
|
||||
queryFn: async () => {
|
||||
const response = await apiRequest("GET", "/api/admin/stats", undefined, {
|
||||
'X-Admin-Key': adminKey
|
||||
});
|
||||
return response.json();
|
||||
},
|
||||
});
|
||||
|
||||
// Mutations
|
||||
const deletePixelMutation = useMutation({
|
||||
mutationFn: async (pixelId: string) => {
|
||||
const response = await apiRequest("DELETE", `/api/admin/pixels/${pixelId}`, undefined, {
|
||||
'X-Admin-Key': adminKey
|
||||
});
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/pixels'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/admin/stats'] });
|
||||
toast({ title: "Pixel gelöscht" });
|
||||
},
|
||||
});
|
||||
|
||||
const clearCanvasMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const response = await apiRequest("DELETE", "/api/admin/canvas", undefined, {
|
||||
'X-Admin-Key': adminKey
|
||||
});
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/pixels'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/admin/stats'] });
|
||||
toast({ title: "Canvas geleert" });
|
||||
},
|
||||
});
|
||||
|
||||
const expandCanvasMutation = useMutation({
|
||||
mutationFn: async ({ width, height }: { width: number; height: number }) => {
|
||||
const response = await apiRequest("POST", "/api/config/expand", {
|
||||
canvasWidth: width,
|
||||
canvasHeight: height
|
||||
}, {
|
||||
'X-Admin-Key': adminKey
|
||||
});
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/config'] });
|
||||
toast({ title: "Canvas erweitert" });
|
||||
},
|
||||
});
|
||||
|
||||
const exportCanvasMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const response = await apiRequest("POST", "/api/admin/export", undefined, {
|
||||
'X-Admin-Key': adminKey
|
||||
});
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
toast({ title: "Canvas exportiert", description: `Datei: ${data.filename}` });
|
||||
},
|
||||
});
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className="min-h-screen bg-canvas-bg text-white flex items-center justify-center">
|
||||
<Card className="w-96 bg-panel-bg border-gray-700">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Admin-Zugang</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Admin-Schlüssel eingeben"
|
||||
value={adminKey}
|
||||
onChange={(e) => setAdminKey(e.target.value)}
|
||||
className="bg-panel-hover border-gray-600 text-white"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => checkAuth(adminKey)}
|
||||
className="w-full"
|
||||
disabled={!adminKey}
|
||||
>
|
||||
Anmelden
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-canvas-bg text-white">
|
||||
<AuthBanner />
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-3xl font-bold">Admin-Panel</h1>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsAuthenticated(false);
|
||||
localStorage.removeItem("adminKey");
|
||||
}}
|
||||
>
|
||||
Abmelden
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="overview" className="space-y-6">
|
||||
<TabsList className="bg-panel-bg">
|
||||
<TabsTrigger value="overview">Übersicht</TabsTrigger>
|
||||
<TabsTrigger value="moderation">Moderation</TabsTrigger>
|
||||
<TabsTrigger value="canvas">Canvas-Verwaltung</TabsTrigger>
|
||||
<TabsTrigger value="export">Export</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Card className="bg-panel-bg border-gray-700">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Statistiken</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-white">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span>Gesamte Pixel:</span>
|
||||
<Badge>{adminStats?.totalPixels || 0}</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Einzigartige Nutzer:</span>
|
||||
<Badge>{adminStats?.uniqueUsers || 0}</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Canvas-Größe:</span>
|
||||
<Badge>{config?.canvasWidth}×{config?.canvasHeight}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-panel-bg border-gray-700">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Letzte Aktivität</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-white">
|
||||
<div className="space-y-2 max-h-40 overflow-y-auto">
|
||||
{pixels.slice(0, 5).map((pixel) => (
|
||||
<div key={pixel.id} className="text-sm">
|
||||
<span className="text-muted">{pixel.username}</span> -
|
||||
<span className="ml-1">({pixel.x}, {pixel.y})</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-panel-bg border-gray-700">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">System</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-white">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span>Cooldown:</span>
|
||||
<Badge>{config?.defaultCooldown}s</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Auto-Events:</span>
|
||||
<Badge variant={config?.enableAutomaticEvents ? "default" : "secondary"}>
|
||||
{config?.enableAutomaticEvents ? "An" : "Aus"}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="moderation">
|
||||
<div className="space-y-6">
|
||||
<Card className="bg-panel-bg border-gray-700">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Pixel-Moderation</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex space-x-2">
|
||||
<Input
|
||||
placeholder="Pixel-ID eingeben"
|
||||
value={selectedPixelId}
|
||||
onChange={(e) => setSelectedPixelId(e.target.value)}
|
||||
className="bg-panel-hover border-gray-600 text-white"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => deletePixelMutation.mutate(selectedPixelId)}
|
||||
disabled={!selectedPixelId}
|
||||
variant="destructive"
|
||||
>
|
||||
Pixel löschen
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-600 pt-4">
|
||||
<Button
|
||||
onClick={() => clearCanvasMutation.mutate()}
|
||||
variant="destructive"
|
||||
className="w-full"
|
||||
>
|
||||
Gesamtes Canvas leeren
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-panel-bg border-gray-700">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Pixel-Liste</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="max-h-60 overflow-y-auto space-y-2">
|
||||
{pixels.slice().reverse().slice(0, 20).map((pixel) => (
|
||||
<div key={pixel.id} className="flex items-center justify-between p-2 bg-panel-hover rounded">
|
||||
<div className="text-white text-sm">
|
||||
<span className="font-mono">{pixel.id.slice(0, 8)}</span> -
|
||||
<span className="ml-2">{pixel.username}</span> -
|
||||
<span className="ml-2">({pixel.x}, {pixel.y})</span> -
|
||||
<span
|
||||
className="ml-2 inline-block w-4 h-4 rounded"
|
||||
style={{ backgroundColor: pixel.color }}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => deletePixelMutation.mutate(pixel.id)}
|
||||
>
|
||||
Löschen
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="canvas">
|
||||
<Card className="bg-panel-bg border-gray-700">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Canvas-Verwaltung</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-white text-sm">Breite</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={canvasWidth}
|
||||
onChange={(e) => setCanvasWidth(Number(e.target.value))}
|
||||
className="bg-panel-hover border-gray-600 text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-white text-sm">Höhe</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={canvasHeight}
|
||||
onChange={(e) => setCanvasHeight(Number(e.target.value))}
|
||||
className="bg-panel-hover border-gray-600 text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => expandCanvasMutation.mutate({
|
||||
width: canvasWidth,
|
||||
height: canvasHeight
|
||||
})}
|
||||
className="w-full"
|
||||
>
|
||||
Canvas erweitern
|
||||
</Button>
|
||||
|
||||
<div className="text-sm text-muted">
|
||||
Aktuelle Größe: {config?.canvasWidth}×{config?.canvasHeight}
|
||||
<br />
|
||||
Hinweis: Canvas kann nur erweitert, nicht verkleinert werden.
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="export">
|
||||
<Card className="bg-panel-bg border-gray-700">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Export-Verwaltung</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Button
|
||||
onClick={() => exportCanvasMutation.mutate()}
|
||||
className="w-full"
|
||||
>
|
||||
Manueller SVG-Export
|
||||
</Button>
|
||||
|
||||
<div className="text-sm text-muted">
|
||||
Automatische Exports erfolgen alle {config?.exportInterval || 60} Sekunden.
|
||||
Dateien werden im exports/ Verzeichnis gespeichert.
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -43,13 +43,15 @@ export default function CanvasPage() {
|
||||
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;
|
||||
|
||||
|
||||
// Aktualisiere Cooldown nach jedem Pixel-Placement (auch von anderen Nutzern)
|
||||
// um sicherzustellen, dass lokaler State mit Server synchron ist
|
||||
if (userId) {
|
||||
fetch(`/api/cooldown/${userId}`)
|
||||
.then(res => res.json())
|
||||
.then(data => setCooldownSeconds(data.remainingSeconds))
|
||||
.catch(error => console.error("Failed to sync cooldown:", error));
|
||||
}
|
||||
break;
|
||||
|
||||
case "cooldown_update":
|
||||
if (message.data.userId === userId) {
|
||||
@@ -67,12 +69,21 @@ export default function CanvasPage() {
|
||||
const response = await apiRequest("POST", "/api/pixels", pixel);
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
onSuccess: async () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/pixels'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/recent'] });
|
||||
// Start cooldown countdown
|
||||
if (config && !config.enableAutomaticEvents) {
|
||||
setCooldownSeconds(config.defaultCooldown);
|
||||
|
||||
// Hole den aktuellen Cooldown-Status vom Server
|
||||
try {
|
||||
const cooldownResponse = await fetch(`/api/cooldown/${userId}`);
|
||||
const cooldownData = await cooldownResponse.json();
|
||||
setCooldownSeconds(cooldownData.remainingSeconds);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch cooldown after placement:", error);
|
||||
// Fallback: Setze Standard-Cooldown wenn Server-Abfrage fehlschlägt
|
||||
if (config && !config.enableAutomaticEvents) {
|
||||
setCooldownSeconds(config.defaultCooldown);
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
@@ -118,10 +129,14 @@ export default function CanvasPage() {
|
||||
}
|
||||
};
|
||||
|
||||
if (userId) {
|
||||
if (userId && config) {
|
||||
fetchCooldown();
|
||||
|
||||
// Aktualisiere Cooldown regelmäßig für den Fall, dass er anderswo gesetzt wurde
|
||||
const interval = setInterval(fetchCooldown, 2000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [userId]);
|
||||
}, [userId, config]);
|
||||
|
||||
const handlePixelClick = (x: number, y: number) => {
|
||||
if (cooldownSeconds > 0) return;
|
||||
@@ -160,9 +175,14 @@ export default function CanvasPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
|
||||
|
||||
|
||||
{/* Admin Link */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => window.location.href = '/admin'}
|
||||
>
|
||||
Admin
|
||||
</Button>
|
||||
|
||||
{/* User Info */}
|
||||
<div className="flex items-center space-x-2 px-3 py-2 bg-panel-hover rounded-lg">
|
||||
|
||||
Reference in New Issue
Block a user