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,7 +2,8 @@
|
|||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { useState } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
const [queryClient] = useState(
|
||||
|
|
@ -16,9 +17,29 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|||
})
|
||||
);
|
||||
|
||||
const [theme, setTheme] = useState<"light" | "dark">("dark");
|
||||
useEffect(() => {
|
||||
const sync = () => {
|
||||
setTheme(document.documentElement.classList.contains("dark") ? "dark" : "light");
|
||||
};
|
||||
sync();
|
||||
const obs = new MutationObserver(sync);
|
||||
obs.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
|
||||
return () => obs.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SessionProvider>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
<Toaster
|
||||
theme={theme}
|
||||
position="bottom-right"
|
||||
richColors
|
||||
closeButton
|
||||
toastOptions={{ duration: 4000 }}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</SessionProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue