UX polish pass 2: toasts, optimistic updates, mod update detection

- Install sonner; <Toaster> mounted in Providers, auto-tracks theme.
- Toasts replace inline result Alerts across ServerControls, PlayerManager,
  BackupManager, ModManager (install/remove/restore/delete/start/stop).
- PlayerManager: optimistic op/deop/whitelist/ban/pardon via onMutate +
  rollback; UI updates instantly before RCON round-trip.
- Modrinth search results now show author + "updated Xd ago" with full
  timestamp on hover; downloads on its own row.
- New /api/mods/updates endpoint: per-installed-mod Modrinth latest-version
  lookup (parallel, 30min memo). Amber "Update available" badge rendered
  next to installed mod rows when filenames differ.
- PlayerAvatar + Modrinth icons migrated to next/image (unoptimized, size
  hints) — fewer layout shifts.
- Login page surfaces ?error= + NextAuth error codes (CredentialsSignin,
  SessionRequired, etc.), preserves callbackUrl, adds autocomplete hints
  and role="alert". Wrapped in Suspense per Next 16 requirement.
- Snapshots + backups show relative "Xh ago" with exact timestamp on hover
  via new lib/time.ts helper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hurkicorgi 2026-04-13 05:11:17 -06:00
parent 6c91f7fef0
commit f9ae1afac1
12 changed files with 334 additions and 76 deletions

View file

@ -2,6 +2,7 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Separator } from "@/components/ui/separator";
@ -51,16 +52,21 @@ export function ServerControls() {
}
return { ...(await res.json()), act };
},
onSuccess: () => {
onSuccess: (_, act) => {
toast.success(`${ACTION_LABEL[act]} command sent`, {
description: "Status will update in a few seconds.",
});
setTimeout(
() => queryClient.invalidateQueries({ queryKey: ["status"] }),
3000
);
},
onError: (err, act) => {
toast.error(`${ACTION_LABEL[act]} failed`, { description: err.message });
},
});
const isOnline = status?.online ?? false;
const lastAction = action.data?.act as Action | undefined;
const trigger = (act: Action) => {
if (act === "start") {
@ -179,20 +185,7 @@ export function ServerControls() {
</AlertDescription>
</Alert>
)}
{action.isSuccess && !action.isPending && (
<Alert className="border-emerald-500/20 bg-emerald-500/5">
<AlertDescription className="text-emerald-300">
{lastAction ? `${ACTION_LABEL[lastAction]} command sent.` : "Command sent."} Status updates in a few seconds.
</AlertDescription>
</Alert>
)}
{action.isError && (
<Alert className="border-red-500/20 bg-red-500/5">
<AlertDescription className="text-red-300">
{action.error.message}
</AlertDescription>
</Alert>
)}
{/* success/error surfaced via toast */}
<Separator />