Initial push
This commit is contained in:
362
app/(dashboard)/server/page.tsx
Normal file
362
app/(dashboard)/server/page.tsx
Normal file
@@ -0,0 +1,362 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Settings, RefreshCw, Download, Server, Map } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface ServerSettings {
|
||||
minecraftPath?: string;
|
||||
serverJar?: string;
|
||||
serverVersion?: string;
|
||||
serverType?: string;
|
||||
maxRam?: number;
|
||||
minRam?: number;
|
||||
rconEnabled?: boolean;
|
||||
rconPort?: number;
|
||||
javaArgs?: string;
|
||||
autoStart?: boolean;
|
||||
restartOnCrash?: boolean;
|
||||
backupEnabled?: boolean;
|
||||
backupSchedule?: string;
|
||||
bluemapEnabled?: boolean;
|
||||
bluemapUrl?: string;
|
||||
}
|
||||
|
||||
const SERVER_TYPES = ["vanilla", "paper", "spigot", "bukkit", "fabric", "forge", "bedrock"];
|
||||
|
||||
export default function ServerPage() {
|
||||
const [settings, setSettings] = useState<ServerSettings>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [versions, setVersions] = useState<string[]>([]);
|
||||
const [loadingVersions, setLoadingVersions] = useState(false);
|
||||
const [selectedType, setSelectedType] = useState("paper");
|
||||
|
||||
const fetchSettings = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/server/settings");
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data.settings) {
|
||||
setSettings(data.settings);
|
||||
setSelectedType(data.settings.serverType ?? "paper");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchVersions = useCallback(async (type: string) => {
|
||||
setLoadingVersions(true);
|
||||
try {
|
||||
const res = await fetch(`/api/server/versions?type=${type}`);
|
||||
if (res.ok) setVersions((await res.json()).versions);
|
||||
} finally {
|
||||
setLoadingVersions(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { fetchSettings(); }, [fetchSettings]);
|
||||
useEffect(() => { fetchVersions(selectedType); }, [selectedType, fetchVersions]);
|
||||
|
||||
const save = async (updates: Partial<ServerSettings>) => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const res = await fetch("/api/server/settings", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
if (!res.ok) throw new Error((await res.json()).error);
|
||||
setSettings((prev) => ({ ...prev, ...updates }));
|
||||
toast.success("Settings saved");
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "Failed to save");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<Skeleton className="h-8 w-48 bg-zinc-800" />
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-48 w-full bg-zinc-800 rounded-xl" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">Server Settings</h1>
|
||||
<p className="text-zinc-400 text-sm mt-1">
|
||||
Configure your Minecraft server and CubeAdmin options
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="general" className="space-y-4">
|
||||
<TabsList className="bg-zinc-900 border border-zinc-800">
|
||||
<TabsTrigger value="general" className="data-[state=active]:bg-zinc-800 data-[state=active]:text-white text-zinc-400">
|
||||
General
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="performance" className="data-[state=active]:bg-zinc-800 data-[state=active]:text-white text-zinc-400">
|
||||
Performance
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="updates" className="data-[state=active]:bg-zinc-800 data-[state=active]:text-white text-zinc-400">
|
||||
Updates
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="integrations" className="data-[state=active]:bg-zinc-800 data-[state=active]:text-white text-zinc-400">
|
||||
Integrations
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* General */}
|
||||
<TabsContent value="general" className="space-y-4">
|
||||
<Card className="bg-zinc-900 border-zinc-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base font-medium text-zinc-300 flex items-center gap-2">
|
||||
<Server className="w-4 h-4 text-emerald-500" />
|
||||
Server Configuration
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-zinc-300">Minecraft Server Path</Label>
|
||||
<Input
|
||||
value={settings.minecraftPath ?? ""}
|
||||
onChange={(e) => setSettings(p => ({ ...p, minecraftPath: e.target.value }))}
|
||||
placeholder="/opt/minecraft/server"
|
||||
className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500 font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-zinc-300">Server JAR filename</Label>
|
||||
<Input
|
||||
value={settings.serverJar ?? ""}
|
||||
onChange={(e) => setSettings(p => ({ ...p, serverJar: e.target.value }))}
|
||||
placeholder="server.jar"
|
||||
className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500 font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-zinc-800 border border-zinc-700">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-zinc-300">Auto-start on boot</p>
|
||||
<p className="text-xs text-zinc-500 mt-0.5">Start server when CubeAdmin starts</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.autoStart ?? false}
|
||||
onCheckedChange={(v) => save({ autoStart: v })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-zinc-800 border border-zinc-700">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-zinc-300">Auto-restart on crash</p>
|
||||
<p className="text-xs text-zinc-500 mt-0.5">Automatically restart if server crashes</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.restartOnCrash ?? false}
|
||||
onCheckedChange={(v) => save({ restartOnCrash: v })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => save({ minecraftPath: settings.minecraftPath, serverJar: settings.serverJar })}
|
||||
disabled={saving}
|
||||
className="bg-emerald-600 hover:bg-emerald-500 text-white"
|
||||
size="sm"
|
||||
>
|
||||
{saving ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Performance */}
|
||||
<TabsContent value="performance" className="space-y-4">
|
||||
<Card className="bg-zinc-900 border-zinc-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base font-medium text-zinc-300">JVM Settings</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-zinc-300">Min RAM (MB)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={settings.minRam ?? 512}
|
||||
onChange={(e) => setSettings(p => ({ ...p, minRam: parseInt(e.target.value) }))}
|
||||
className="bg-zinc-800 border-zinc-700 text-white"
|
||||
min={256}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-zinc-300">Max RAM (MB)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={settings.maxRam ?? 2048}
|
||||
onChange={(e) => setSettings(p => ({ ...p, maxRam: parseInt(e.target.value) }))}
|
||||
className="bg-zinc-800 border-zinc-700 text-white"
|
||||
min={512}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-zinc-300">Additional Java Arguments</Label>
|
||||
<Input
|
||||
value={settings.javaArgs ?? ""}
|
||||
onChange={(e) => setSettings(p => ({ ...p, javaArgs: e.target.value }))}
|
||||
placeholder="-XX:+UseG1GC -XX:MaxGCPauseMillis=200"
|
||||
className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500 font-mono text-sm"
|
||||
/>
|
||||
<p className="text-xs text-zinc-500">
|
||||
Aikar's flags are applied by default with the Docker image.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => save({ minRam: settings.minRam, maxRam: settings.maxRam, javaArgs: settings.javaArgs })}
|
||||
disabled={saving}
|
||||
className="bg-emerald-600 hover:bg-emerald-500 text-white"
|
||||
size="sm"
|
||||
>
|
||||
{saving ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Updates */}
|
||||
<TabsContent value="updates" className="space-y-4">
|
||||
<Card className="bg-zinc-900 border-zinc-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base font-medium text-zinc-300 flex items-center gap-2">
|
||||
<Download className="w-4 h-4 text-emerald-500" />
|
||||
Server Version
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-zinc-300">Server Type</Label>
|
||||
<Select
|
||||
value={selectedType}
|
||||
onValueChange={(v) => { if (v) { setSelectedType(v); fetchVersions(v); } }}
|
||||
>
|
||||
<SelectTrigger className="bg-zinc-800 border-zinc-700 text-white">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-zinc-800 border-zinc-700">
|
||||
{SERVER_TYPES.map((t) => (
|
||||
<SelectItem key={t} value={t} className="text-zinc-300 focus:bg-zinc-700 focus:text-white capitalize">
|
||||
{t.charAt(0).toUpperCase() + t.slice(1)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-zinc-300">Version</Label>
|
||||
<Select
|
||||
value={settings.serverVersion ?? ""}
|
||||
onValueChange={(v) => setSettings(p => ({ ...p, serverVersion: v ?? undefined }))}
|
||||
disabled={loadingVersions}
|
||||
>
|
||||
<SelectTrigger className="bg-zinc-800 border-zinc-700 text-white">
|
||||
<SelectValue placeholder={loadingVersions ? "Loading..." : "Select version"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-zinc-800 border-zinc-700 max-h-60">
|
||||
{versions.map((v) => (
|
||||
<SelectItem key={v} value={v} className="text-zinc-300 focus:bg-zinc-700 focus:text-white">
|
||||
{v}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 rounded-lg bg-amber-500/10 border border-amber-500/30 text-amber-400 text-sm">
|
||||
<span>⚠️</span>
|
||||
<p>Changing server version requires a server restart. Always backup first!</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => save({ serverType: selectedType, serverVersion: settings.serverVersion })}
|
||||
disabled={saving || !settings.serverVersion}
|
||||
className="bg-emerald-600 hover:bg-emerald-500 text-white"
|
||||
size="sm"
|
||||
>
|
||||
{saving ? "Saving..." : "Apply Version"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Integrations */}
|
||||
<TabsContent value="integrations" className="space-y-4">
|
||||
<Card className="bg-zinc-900 border-zinc-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base font-medium text-zinc-300 flex items-center gap-2">
|
||||
<Map className="w-4 h-4 text-emerald-500" />
|
||||
BlueMap Integration
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-zinc-800 border border-zinc-700">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-zinc-300">Enable BlueMap</p>
|
||||
<p className="text-xs text-zinc-500 mt-0.5">Show the 3D map in the Map section</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.bluemapEnabled ?? false}
|
||||
onCheckedChange={(v) => save({ bluemapEnabled: v })}
|
||||
/>
|
||||
</div>
|
||||
{settings.bluemapEnabled && (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-zinc-300">BlueMap URL</Label>
|
||||
<Input
|
||||
value={settings.bluemapUrl ?? ""}
|
||||
onChange={(e) => setSettings(p => ({ ...p, bluemapUrl: e.target.value }))}
|
||||
placeholder="http://localhost:8100"
|
||||
className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500 font-mono text-sm"
|
||||
/>
|
||||
<p className="text-xs text-zinc-500">
|
||||
The URL where BlueMap is accessible from your browser.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => save({ bluemapEnabled: settings.bluemapEnabled, bluemapUrl: settings.bluemapUrl })}
|
||||
disabled={saving}
|
||||
className="bg-emerald-600 hover:bg-emerald-500 text-white"
|
||||
size="sm"
|
||||
>
|
||||
{saving ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user