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:
parent
6c91f7fef0
commit
f9ae1afac1
12 changed files with 334 additions and 76 deletions
|
|
@ -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 />
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue