Files
CubeAdmin/app/(dashboard)/updates/page.tsx
2026-03-08 17:01:36 +01:00

255 lines
9.1 KiB
TypeScript

"use client";
import { useState, useEffect, useCallback } from "react";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Download, RefreshCw, AlertTriangle, CheckCircle2, Server } from "lucide-react";
import { toast } from "sonner";
const SERVER_TYPES = ["vanilla", "paper", "fabric"] as const;
type ServerType = (typeof SERVER_TYPES)[number];
interface Settings {
serverType?: string;
serverVersion?: string;
serverJar?: string;
}
export default function UpdatesPage() {
const [settings, setSettings] = useState<Settings | null>(null);
const [selectedType, setSelectedType] = useState<ServerType>("paper");
const [versions, setVersions] = useState<string[]>([]);
const [selectedVersion, setSelectedVersion] = useState("");
const [loadingSettings, setLoadingSettings] = useState(true);
const [loadingVersions, setLoadingVersions] = useState(false);
const [saving, setSaving] = useState(false);
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 as ServerType) ?? "paper");
setSelectedVersion(data.settings.serverVersion ?? "");
}
}
} finally {
setLoadingSettings(false);
}
}, []);
const fetchVersions = useCallback(async (type: string) => {
setLoadingVersions(true);
setVersions([]);
try {
const res = await fetch(`/api/server/versions?type=${type}`);
if (res.ok) {
const data = await res.json();
setVersions(data.versions ?? []);
} else {
toast.error("Failed to fetch versions");
}
} catch {
toast.error("Network error fetching versions");
} finally {
setLoadingVersions(false);
}
}, []);
useEffect(() => {
fetchSettings();
}, [fetchSettings]);
useEffect(() => {
fetchVersions(selectedType);
}, [selectedType, fetchVersions]);
const isUpToDate =
settings?.serverVersion === selectedVersion &&
settings?.serverType === selectedType;
async function handleApply() {
if (!selectedVersion) {
toast.error("Please select a version");
return;
}
setSaving(true);
try {
const res = await fetch("/api/server/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ serverType: selectedType, serverVersion: selectedVersion }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error ?? "Failed to apply");
}
setSettings((prev) => ({ ...prev, serverType: selectedType, serverVersion: selectedVersion }));
toast.success(`Server version set to ${selectedType} ${selectedVersion}`);
} catch (err) {
toast.error(err instanceof Error ? err.message : "Failed to apply version");
} finally {
setSaving(false);
}
}
const currentVersion = settings?.serverVersion
? `${settings.serverType ?? "unknown"} ${settings.serverVersion}`
: "Not configured";
return (
<div className="p-6 max-w-2xl space-y-6">
<div>
<h1 className="text-2xl font-bold text-white">Server Updates</h1>
<p className="text-zinc-400 text-sm mt-1">
Manage your Minecraft server version
</p>
</div>
{/* Current version */}
<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" />
Current Version
</CardTitle>
</CardHeader>
<CardContent>
{loadingSettings ? (
<Skeleton className="h-6 w-48 bg-zinc-800" />
) : (
<div className="flex items-center gap-3">
<span className="text-white font-mono text-sm">{currentVersion}</span>
{settings?.serverVersion && (
<Badge
className={
isUpToDate
? "bg-emerald-500/10 text-emerald-400 border-emerald-500/20"
: "bg-amber-500/10 text-amber-400 border-amber-500/20"
}
>
{isUpToDate ? "Up to date" : "Update available"}
</Badge>
)}
</div>
)}
</CardContent>
</Card>
{/* Version picker */}
<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" />
Select Version
</CardTitle>
<CardDescription className="text-zinc-500">
Choose a server type and version to apply
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<label className="text-sm text-zinc-300">Server Type</label>
<Select
value={selectedType}
onValueChange={(v) => { if (v) setSelectedType(v as ServerType); }}
>
<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-sm text-zinc-300">Version</label>
<Select
value={selectedVersion}
onValueChange={(v) => { if (v) setSelectedVersion(v); }}
disabled={loadingVersions || versions.length === 0}
>
<SelectTrigger className="bg-zinc-800 border-zinc-700 text-white">
<SelectValue
placeholder={
loadingVersions
? "Loading…"
: versions.length === 0
? "No versions found"
: "Select version"
}
/>
</SelectTrigger>
<SelectContent className="bg-zinc-800 border-zinc-700 max-h-64">
{versions.map((v) => (
<SelectItem key={v} value={v} className="text-zinc-300 focus:bg-zinc-700 focus:text-white font-mono text-sm">
{v}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="flex items-center gap-2">
<Button
onClick={handleApply}
disabled={saving || !selectedVersion || loadingVersions}
className="bg-emerald-600 hover:bg-emerald-500 text-white"
size="sm"
>
<Download className="w-3.5 h-3.5 mr-1.5" />
{saving ? "Applying…" : "Apply Version"}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => fetchVersions(selectedType)}
disabled={loadingVersions}
className="border-zinc-700 text-zinc-300 hover:bg-zinc-800"
>
<RefreshCw className={`w-3.5 h-3.5 mr-1.5 ${loadingVersions ? "animate-spin" : ""}`} />
Refresh
</Button>
</div>
{selectedVersion && settings?.serverVersion && selectedVersion !== settings.serverVersion && (
<div className="flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-amber-400 text-sm">
<AlertTriangle className="w-4 h-4 mt-0.5 shrink-0" />
<p>
Changing from <span className="font-mono">{settings.serverVersion}</span> to{" "}
<span className="font-mono">{selectedVersion}</span> requires a server restart.
Make sure to create a backup first.
</p>
</div>
)}
{isUpToDate && selectedVersion && (
<div className="flex items-center gap-2 rounded-lg border border-emerald-500/30 bg-emerald-500/10 p-3 text-emerald-400 text-sm">
<CheckCircle2 className="w-4 h-4 shrink-0" />
<p>This version is already configured.</p>
</div>
)}
</CardContent>
</Card>
</div>
);
}