feat(admin): nach jeder Aktion automatisch neu laden

AdminAutoRefresh im Admin-Layout lädt nach jeder erfolgreichen Mutation
alle sichtbaren Daten neu (invalidateQueries, nur aktive Queries holen
sofort neu). Damit braucht keine Admin-Seite mehr F5, auch die, die bisher
nach manchen Aktionen gar nicht neu geladen haben. Die ~35 einzelnen
invalidateQueries-Aufrufe in den Seiten entfallen. Gilt nur im Admin-Panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-07 05:50:05 +02:00
1 parent 20d6888449
commit 5f75998512
22 files changed
+101 -103

No files matched your search

+4 -7
View File
@@ -1,7 +1,7 @@
"use client";
import { useState, useRef, useCallback } from "react";
import { useQuery, useQueries, useMutation, useQueryClient } from "@tanstack/react-query";
import { useQuery, useQueries, useMutation } from "@tanstack/react-query";
import { useTRPC } from "@/trpc/client";
import { AdminPageHeader } from "@/components/admin/AdminSection";
import { StatusBadge } from "@/components/admin/StatusBadge";
@@ -44,7 +44,6 @@ const MAX_SIZE = 10 * 1024 * 1024;
export default function AdminAdsPage() {
const trpc = useTRPC();
const qc = useQueryClient();
const [form, setForm] = useState<AdForm>(emptyForm);
const [editingId, setEditingId] = useState<string | null>(null);
const [dialogOpen, setDialogOpen] = useState(false);
@@ -68,18 +67,16 @@ export default function AdminAdsPage() {
breedsBySpecies[speciesId] = breedQueries[i]?.data ?? [];
});
const invalidate = () => void qc.invalidateQueries({ queryKey: trpc.admin.listAds.queryKey() });
const getPresignedUrl = useMutation(trpc.admin.getAdImagePresignedUrl.mutationOptions());
const createAd = useMutation(
trpc.admin.createAd.mutationOptions({ onSuccess: () => { invalidate(); closeDialog(); } })
trpc.admin.createAd.mutationOptions({ onSuccess: () => { closeDialog(); } })
);
const updateAd = useMutation(
trpc.admin.updateAd.mutationOptions({ onSuccess: () => { invalidate(); closeDialog(); } })
trpc.admin.updateAd.mutationOptions({ onSuccess: () => { closeDialog(); } })
);
const deleteAd = useMutation(
trpc.admin.deleteAd.mutationOptions({ onSuccess: () => { invalidate(); setDeleteId(null); } })
trpc.admin.deleteAd.mutationOptions({ onSuccess: () => { setDeleteId(null); } })
);
function openCreate() {
+1 -4
View File
@@ -2,7 +2,7 @@
import { useState } from "react";
import { toast } from "sonner";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useTRPC } from "@/trpc/client";
import { Trash2, Loader2, Plus, Ban } from "lucide-react";
import { Button } from "@/components/ui/button";
@@ -31,7 +31,6 @@ const FILTER_LABEL: Record<TypeFilter, string> = { all: "Alle Arten", ...TYPE_LA
export default function AdminBlacklistPage() {
const trpc = useTRPC();
const qc = useQueryClient();
const [typeFilter, setTypeFilter] = useState<TypeFilter>("all");
const [newType, setNewType] = useState<BlacklistEntryType>(BlacklistEntryType.WORD);
const [newValue, setNewValue] = useState("");
@@ -49,7 +48,6 @@ export default function AdminBlacklistPage() {
const deleteMutation = useMutation(
trpc.admin.deleteBlacklistEntry.mutationOptions({
onSuccess: () => {
void qc.invalidateQueries({ queryKey: trpc.admin.listBlacklist.queryKey() });
setDeleteTarget(null);
},
})
@@ -81,7 +79,6 @@ export default function AdminBlacklistPage() {
}
setIsBulkAdding(false);
void qc.invalidateQueries({ queryKey: trpc.admin.listBlacklist.queryKey() });
setNewValue("");
if (addedCount > 0) {
+1 -4
View File
@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useQuery, useMutation } from "@tanstack/react-query";
import { useTRPC } from "@/trpc/client";
import { Send, Loader2, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
@@ -23,7 +23,6 @@ function formatDate(d: string | Date) {
export default function AdminBroadcastsPage() {
const trpc = useTRPC();
const qc = useQueryClient();
const [title, setTitle] = useState("");
const [body, setBody] = useState("");
const [deleteTarget, setDeleteTarget] = useState<{ id: string; title: string } | null>(null);
@@ -34,7 +33,6 @@ export default function AdminBroadcastsPage() {
const createMutation = useMutation(
trpc.admin.createBroadcast.mutationOptions({
onSuccess: () => {
void qc.invalidateQueries({ queryKey: trpc.admin.listBroadcasts.queryKey() });
toast.success("Mitteilung gesendet – Halter sehen sie beim nächsten Öffnen der App");
setTitle("");
setBody("");
@@ -46,7 +44,6 @@ export default function AdminBroadcastsPage() {
const deleteMutation = useMutation(
trpc.admin.deleteBroadcast.mutationOptions({
onSuccess: () => {
void qc.invalidateQueries({ queryKey: trpc.admin.listBroadcasts.queryKey() });
toast.success("Mitteilung gelöscht");
setDeleteTarget(null);
setDeleteReason("");
+1 -3
View File
@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMutation, useQuery } from "@tanstack/react-query";
import { Loader2, Sprout } from "lucide-react";
import { toast } from "sonner";
import { useTRPC } from "@/trpc/client";
@@ -55,7 +55,6 @@ export default function AdminCuratorPage() {
function CuratorForm({ initial }: { initial: Form }) {
const trpc = useTRPC();
const qc = useQueryClient();
const [form, setForm] = useState<Form>({
welcomeEnabled: initial.welcomeEnabled,
autoFollowEnabled: initial.autoFollowEnabled,
@@ -68,7 +67,6 @@ function CuratorForm({ initial }: { initial: Form }) {
trpc.curatorAdmin.update.mutationOptions({
onSuccess: () => {
toast.success("Begrüßung gespeichert");
void qc.invalidateQueries(trpc.curatorAdmin.get.queryFilter());
},
onError: (err) => toast.error(err.message),
})
+2
View File
@@ -6,6 +6,7 @@ import { createTRPCCaller } from "@/trpc/server";
import { timingSafeStringEqual } from "@/lib/timing-safe-compare";
import { AdminNavList } from "@/components/admin/AdminNavList";
import { AdminTopbar } from "@/components/admin/AdminTopbar";
import { AdminAutoRefresh } from "@/components/admin/AdminAutoRefresh";
import { ADMIN_ROOT_ID, ADMIN_THEME_COOKIE, type AdminTheme } from "@/components/admin/admin-theme";
import { ADMIN_ROLE_LABEL, buildAdminNav } from "@/components/admin/admin-nav";
import { ClerkThemeProvider } from "@/components/providers/ClerkThemeProvider";
@@ -56,6 +57,7 @@ export default async function AdminLayout({ children, params }: AdminLayoutProps
// SpeedUp-Plan P0 deliberately kept off non-admin routes — acceptable here
// since the admin panel is an internal ops tool, not perf-critical.
<ClerkThemeProvider>
<AdminAutoRefresh />
{/* Eigene Admin-Palette (globals.css .admin-theme), hell als Standard,
dunkel per Umschalter in der Kopfzeile — unabhängig vom Website-Theme. */}
<div
+1 -3
View File
@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useQuery, useMutation } from "@tanstack/react-query";
import { useTRPC } from "@/trpc/client";
import { Send, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
@@ -33,7 +33,6 @@ function formatDate(d: string | Date) {
export default function AdminLegalPage() {
const trpc = useTRPC();
const qc = useQueryClient();
const [docType, setDocType] = useState<"NUTZUNGSBEDINGUNGEN" | "DATENSCHUTZ">("NUTZUNGSBEDINGUNGEN");
const [changeSummary, setChangeSummary] = useState("");
@@ -42,7 +41,6 @@ export default function AdminLegalPage() {
const publishMutation = useMutation(
trpc.legal.publish.mutationOptions({
onSuccess: () => {
void qc.invalidateQueries({ queryKey: trpc.legal.listVersions.queryKey() });
toast.success("Veröffentlicht – Halter sehen den Hinweis beim nächsten Öffnen der App");
setChangeSummary("");
},
+1 -3
View File
@@ -2,7 +2,7 @@
import { useState } from "react";
import Link from "next/link";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMutation, useQuery } from "@tanstack/react-query";
import { ExternalLink, Loader2, Mail, MapPin, Phone, Search, Users } from "lucide-react";
import { toast } from "sonner";
import { useTRPC } from "@/trpc/client";
@@ -27,7 +27,6 @@ const DATE_FORMAT = new Intl.DateTimeFormat("de-DE", { dateStyle: "medium", time
*/
export default function AdminLostReportsPage() {
const trpc = useTRPC();
const qc = useQueryClient();
const [status, setStatus] = useState<Status>("ACTIVE");
const [target, setTarget] = useState<{ id: string; name: string; mode: Mode } | null>(null);
const [reason, setReason] = useState("");
@@ -39,7 +38,6 @@ export default function AdminLostReportsPage() {
toast.success(target?.mode === "DELETE" ? "Meldung gelöscht" : "Meldung geschlossen");
setTarget(null);
setReason("");
void qc.invalidateQueries({ queryKey: trpc.lostReportAdmin.list.queryKey() });
},
onError: (err) => toast.error(err.message),
})
-2
View File
@@ -53,8 +53,6 @@ export default function AdminMessagesPage() {
trpc.admin.sendTeamReply.mutationOptions({
onSuccess: () => {
setDraft("");
void qc.invalidateQueries({ queryKey: trpc.admin.getTeamConversationThread.queryKey({ conversationId: activeId ?? "" }) });
void qc.invalidateQueries({ queryKey: trpc.admin.listTeamConversations.queryKey() });
},
})
);
+1 -5
View File
@@ -2,7 +2,7 @@
import { useState } from "react";
import { toast } from "sonner";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useQuery, useMutation } from "@tanstack/react-query";
import { AdminRoleType } from "@prisma/client";
import { Check, Loader2, Plus, Search, Trash2, UserRound } from "lucide-react";
import { useTRPC } from "@/trpc/client";
@@ -30,7 +30,6 @@ const SEARCH_DEBOUNCE_MS = 250;
*/
export default function AdminModeratorsPage() {
const trpc = useTRPC();
const qc = useQueryClient();
const [dialogOpen, setDialogOpen] = useState(false);
const [query, setQuery] = useState("");
const [picked, setPicked] = useState<{ id: string; label: string } | null>(null);
@@ -45,7 +44,6 @@ export default function AdminModeratorsPage() {
);
const actionCountByModeratorId = new Map(throughput.map((t) => [t.moderatorId, t.actionCount]));
const invalidate = () => void qc.invalidateQueries({ queryKey: trpc.admin.listModerators.queryKey() });
const closeGrant = () => {
setDialogOpen(false);
setQuery("");
@@ -56,7 +54,6 @@ export default function AdminModeratorsPage() {
const grantRole = useMutation(
trpc.admin.grantRole.mutationOptions({
onSuccess: () => {
invalidate();
toast.success("Rolle vergeben.");
closeGrant();
},
@@ -66,7 +63,6 @@ export default function AdminModeratorsPage() {
const revokeRole = useMutation(
trpc.admin.revokeRole.mutationOptions({
onSuccess: () => {
invalidate();
toast.success("Rolle entzogen.");
setRevokeTarget(null);
},
+4 -6
View File
@@ -1,7 +1,7 @@
"use client";
import { useState, useRef, useCallback } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useQuery, useMutation } from "@tanstack/react-query";
import { useTRPC } from "@/trpc/client";
import { AdminPageHeader } from "@/components/admin/AdminSection";
import { StatusBadge } from "@/components/admin/StatusBadge";
@@ -73,7 +73,6 @@ function readAudioDuration(file: File): Promise<number> {
export default function AdminMusicPage() {
const trpc = useTRPC();
const qc = useQueryClient();
const [form, setForm] = useState<TrackForm>(emptyForm);
const [editingId, setEditingId] = useState<string | null>(null);
const [dialogOpen, setDialogOpen] = useState(false);
@@ -86,18 +85,17 @@ export default function AdminMusicPage() {
const { data: rawTracks, isLoading } = useQuery(trpc.admin.listMusicTracks.queryOptions(undefined));
const tracks = rawTracks ?? [];
const invalidate = () => void qc.invalidateQueries({ queryKey: trpc.admin.listMusicTracks.queryKey() });
const getUploadUrl = useMutation(trpc.admin.getMusicTrackUploadUrl.mutationOptions());
const createTrack = useMutation(
trpc.admin.createMusicTrack.mutationOptions({ onSuccess: () => { invalidate(); closeDialog(); } })
trpc.admin.createMusicTrack.mutationOptions({ onSuccess: () => { closeDialog(); } })
);
const updateTrack = useMutation(
trpc.admin.updateMusicTrack.mutationOptions({ onSuccess: () => { invalidate(); closeDialog(); } })
trpc.admin.updateMusicTrack.mutationOptions({ onSuccess: () => { closeDialog(); } })
);
const deleteTrack = useMutation(
trpc.admin.deleteMusicTrack.mutationOptions({ onSuccess: () => { invalidate(); setDeleteId(null); } })
trpc.admin.deleteMusicTrack.mutationOptions({ onSuccess: () => { setDeleteId(null); } })
);
function openCreate() {
+4 -29
View File
@@ -1,7 +1,7 @@
"use client";
import { useState, useSyncExternalStore } from "react";
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query";
import { useTRPC } from "@/trpc/client";
import { Trash2, MessageSquare, Loader2, ChevronDown, Play, ImageOff, PawPrint, EyeOff, Eye, Sparkles, Pin, PinOff, Hash, MoreHorizontal, Flag, Search, Repeat2 } from "lucide-react";
import { Button } from "@/components/ui/button";
@@ -57,7 +57,6 @@ function useGridSize(): [GridSize, (size: GridSize) => void] {
export default function AdminPostsPage() {
const trpc = useTRPC();
const qc = useQueryClient();
const [search, setSearch] = useState("");
const [gridSize, handleGridSizeChange] = useGridSize();
const [commentsPostId, setCommentsPostId] = useState<string | null>(null);
@@ -88,7 +87,6 @@ export default function AdminPostsPage() {
const deletePost = useMutation(
trpc.admin.deletePost.mutationOptions({
onSuccess: () => {
void qc.invalidateQueries({ queryKey: trpc.admin.listPosts.queryKey() });
setDeleteTarget(null);
setReason("");
},
@@ -98,9 +96,6 @@ export default function AdminPostsPage() {
const deleteComment = useMutation(
trpc.admin.deleteComment.mutationOptions({
onSuccess: () => {
void qc.invalidateQueries({
queryKey: trpc.admin.listComments.queryKey({ postId: commentsPostId ?? "" }),
});
setDeleteTarget(null);
setReason("");
},
@@ -110,43 +105,23 @@ export default function AdminPostsPage() {
const hidePost = useMutation(
trpc.admin.hidePost.mutationOptions({
onSuccess: () => {
void qc.invalidateQueries({ queryKey: trpc.admin.listPosts.queryKey() });
setHideTargetId(null);
setHideReason("");
},
})
);
const unhidePost = useMutation(
trpc.admin.unhidePost.mutationOptions({
onSuccess: () => {
void qc.invalidateQueries({ queryKey: trpc.admin.listPosts.queryKey() });
},
})
);
const unhidePost = useMutation(trpc.admin.unhidePost.mutationOptions());
const pinPost = useMutation(
trpc.admin.pinPost.mutationOptions({
onSuccess: () => {
void qc.invalidateQueries({ queryKey: trpc.admin.listPosts.queryKey() });
},
})
);
const pinPost = useMutation(trpc.admin.pinPost.mutationOptions());
const unpinPost = useMutation(
trpc.admin.unpinPost.mutationOptions({
onSuccess: () => {
void qc.invalidateQueries({ queryKey: trpc.admin.listPosts.queryKey() });
},
})
);
const unpinPost = useMutation(trpc.admin.unpinPost.mutationOptions());
const { data: trending } = useQuery(trpc.explore.getTrending.queryOptions());
const setAiDisclosure = useMutation(
trpc.admin.setAiDisclosure.mutationOptions({
onSuccess: () => {
void qc.invalidateQueries({ queryKey: trpc.admin.listPosts.queryKey() });
setAiTargetId(null);
setAiReason("");
},
+3 -7
View File
@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useQuery, useMutation } from "@tanstack/react-query";
import { useTRPC } from "@/trpc/client";
import { getMediaUrl } from "@/lib/media-url";
import { BlurImage } from "@/components/ui/blur-image";
@@ -35,7 +35,6 @@ type ReportAction =
export default function AdminReportsPage() {
const trpc = useTRPC();
const qc = useQueryClient();
const [action, setAction] = useState<ReportAction | null>(null);
const [reason, setReason] = useState("");
// found.md: Melder bekommt eine Team-Nachricht (Begründung bzw. Dank).
@@ -58,18 +57,15 @@ export default function AdminReportsPage() {
)
);
const invalidate = () =>
void qc.invalidateQueries({ queryKey: trpc.admin.listReports.queryKey() });
const dismiss = useMutation(
trpc.admin.dismissReport.mutationOptions({
onSuccess: () => { invalidate(); closeDialog(); },
onSuccess: () => { closeDialog(); },
})
);
const deletePost = useMutation(
trpc.admin.deleteReportedPost.mutationOptions({
onSuccess: () => { invalidate(); closeDialog(); },
onSuccess: () => { closeDialog(); },
})
);
+1 -3
View File
@@ -3,7 +3,7 @@
import { useState } from "react";
import { useRouter, useParams, useSearchParams } from "next/navigation";
import { toast } from "sonner";
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query";
import type { inferRouterOutputs } from "@trpc/server";
import { AdminRoleType } from "@prisma/client";
import {
@@ -78,7 +78,6 @@ function UserStatus({ user }: { user: User }) {
*/
export default function AdminUsersPage() {
const trpc = useTRPC();
const qc = useQueryClient();
const router = useRouter();
const params = useParams<{ secret: string }>();
// ?q= kommt von der globalen Suche in der Kopfzeile; ein neuer Treffer ersetzt die Suche.
@@ -120,7 +119,6 @@ export default function AdminUsersPage() {
const verify = useMutation(
trpc.admin.setPetVerified.mutationOptions({
onSuccess: (pet) => {
void qc.invalidateQueries({ queryKey: trpc.admin.listUsers.queryKey() });
toast.success(pet.isVerified ? `${formatPetName(pet)} ist jetzt verifiziert.` : `Verifizierung von ${formatPetName(pet)} entfernt.`);
},
onError: (err) => toast.error(`Verifizierung fehlgeschlagen: ${err.message}`),
+1 -3
View File
@@ -2,7 +2,7 @@
import { useState } from "react";
import Link from "next/link";
import { useInfiniteQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useInfiniteQuery, useMutation } from "@tanstack/react-query";
import { useTRPC } from "@/trpc/client";
import {
Loader2,
@@ -78,7 +78,6 @@ const EMPTY_CHECKLIST: ChecklistState = {
export default function AdminVerificationPage() {
const trpc = useTRPC();
const qc = useQueryClient();
const [status, setStatus] = useState<RequestStatus>("PENDING");
const [reviewTarget, setReviewTarget] = useState<string | null>(null);
const [checklist, setChecklist] = useState<ChecklistState>(EMPTY_CHECKLIST);
@@ -96,7 +95,6 @@ export default function AdminVerificationPage() {
const reviewMutation = useMutation(
trpc.admin.reviewVerificationRequest.mutationOptions({
onSuccess: () => {
void qc.invalidateQueries({ queryKey: trpc.admin.listVerificationRequests.queryKey() });
setReviewTarget(null);
setChecklist(EMPTY_CHECKLIST);
setNote("");
+28
View File
@@ -0,0 +1,28 @@
"use client";
import { useEffect } from "react";
import { useQueryClient, type QueryClient } from "@tanstack/react-query";
/**
* Nach jeder erfolgreichen Aktion alle Daten neu laden. invalidateQueries()
* holt nur die gerade sichtbaren (aktiven) Queries sofort neu, der Rest gilt
* als veraltet und lädt beim nächsten Anzeigen. Gibt die Abmeldung zurück.
*/
export function subscribeAutoRefresh(queryClient: QueryClient): () => void {
return queryClient.getMutationCache().subscribe((event) => {
if (event.type === "updated" && event.action.type === "success") {
void queryClient.invalidateQueries();
}
});
}
/**
* Admin-Panel: keine Seite muss nach einer Aktion selbst neu laden. Hängt nur,
* solange das Admin-Layout gemountet ist — der QueryClient ist app-weit, die
* normale App bleibt davon unberührt.
*/
export function AdminAutoRefresh() {
const queryClient = useQueryClient();
useEffect(() => subscribeAutoRefresh(queryClient), [queryClient]);
return null;
}
@@ -0,0 +1,41 @@
import { describe, it, expect, vi } from "vitest";
import { QueryClient } from "@tanstack/react-query";
import { subscribeAutoRefresh } from "./AdminAutoRefresh";
function runMutation(queryClient: QueryClient, mutationFn: () => Promise<unknown>) {
return queryClient.getMutationCache().build(queryClient, { mutationFn }).execute(undefined);
}
describe("subscribeAutoRefresh", () => {
it("lädt nach einer erfolgreichen Aktion alle Daten neu", async () => {
const queryClient = new QueryClient();
const invalidate = vi.spyOn(queryClient, "invalidateQueries").mockResolvedValue();
subscribeAutoRefresh(queryClient);
await runMutation(queryClient, () => Promise.resolve("ok"));
expect(invalidate).toHaveBeenCalledOnce();
expect(invalidate).toHaveBeenCalledWith();
});
it("lädt nach einer fehlgeschlagenen Aktion nichts neu", async () => {
const queryClient = new QueryClient();
const invalidate = vi.spyOn(queryClient, "invalidateQueries").mockResolvedValue();
subscribeAutoRefresh(queryClient);
await runMutation(queryClient, () => Promise.reject(new Error("FORBIDDEN"))).catch(() => {});
expect(invalidate).not.toHaveBeenCalled();
});
it("hört nach dem Abmelden (Admin-Layout verlassen) auf", async () => {
const queryClient = new QueryClient();
const invalidate = vi.spyOn(queryClient, "invalidateQueries").mockResolvedValue();
const unsubscribe = subscribeAutoRefresh(queryClient);
unsubscribe();
await runMutation(queryClient, () => Promise.resolve("ok"));
expect(invalidate).not.toHaveBeenCalled();
});
});
@@ -1,6 +1,6 @@
"use client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMutation, useQuery } from "@tanstack/react-query";
import { Sparkles, TriangleAlert } from "lucide-react";
import { useTRPC } from "@/trpc/client";
import { Switch } from "@/components/ui/switch";
@@ -35,17 +35,14 @@ function SwitchRow({ icon: Icon, title, description, checked, isDanger, disabled
*/
export function SystemCard({ canToggleMaintenance }: { canToggleMaintenance: boolean }) {
const trpc = useTRPC();
const qc = useQueryClient();
const { data: maintenance } = useQuery(trpc.admin.getMaintenanceMode.queryOptions(undefined, { enabled: canToggleMaintenance }));
const { data: storyEditor } = useQuery(trpc.admin.getStoryEditorEnabled.queryOptions());
const setMaintenance = useMutation(
trpc.admin.setMaintenanceMode.mutationOptions({
onSuccess: () => void qc.invalidateQueries({ queryKey: trpc.admin.getMaintenanceMode.queryKey() }),
})
);
const setStoryEditor = useMutation(
trpc.admin.setStoryEditorEnabled.mutationOptions({
onSuccess: () => void qc.invalidateQueries({ queryKey: trpc.admin.getStoryEditorEnabled.queryKey() }),
})
);
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMutation, useQuery } from "@tanstack/react-query";
import { AlertTriangle, Loader2 } from "lucide-react";
import { toast } from "sonner";
import { useTRPC } from "@/trpc/client";
@@ -31,14 +31,12 @@ function statusOf(o: { referralRevokedAt: MaybeDate; referralQualifiedAt: MaybeD
export function OverviewTab() {
const trpc = useTRPC();
const qc = useQueryClient();
const { data, isLoading } = useQuery(trpc.referralAdmin.overview.queryOptions());
const { data: featurePool = [] } = useQuery(trpc.referralAdmin.perkHolders.queryOptions({ perk: "FEATURE_POOL" }));
const { data: spotlight = [] } = useQuery(trpc.referralAdmin.perkHolders.queryOptions({ perk: "CURATOR_SPOTLIGHT" }));
const refresh = () => void qc.invalidateQueries(trpc.referralAdmin.overview.queryFilter());
const approve = useMutation(trpc.referralAdmin.approveFlagged.mutationOptions({ onSuccess: () => { toast.success("Freigegeben"); refresh(); }, onError: (e) => toast.error(e.message) }));
const revoke = useMutation(trpc.referralAdmin.revokeReferral.mutationOptions({ onSuccess: () => { toast.success("Aberkannt"); refresh(); }, onError: (e) => toast.error(e.message) }));
const approve = useMutation(trpc.referralAdmin.approveFlagged.mutationOptions({ onSuccess: () => { toast.success("Freigegeben") }, onError: (e) => toast.error(e.message) }));
const revoke = useMutation(trpc.referralAdmin.revokeReferral.mutationOptions({ onSuccess: () => { toast.success("Aberkannt") }, onError: (e) => toast.error(e.message) }));
if (isLoading || !data) return <Loader2 className="mx-auto my-12 h-5 w-5 animate-spin text-muted-foreground" />;
+1 -4
View File
@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMutation, useQuery } from "@tanstack/react-query";
import { Loader2, Pencil, Plus } from "lucide-react";
import { toast } from "sonner";
import { useTRPC } from "@/trpc/client";
@@ -15,15 +15,12 @@ type EditorState = { mode: "create" | "edit"; id: string | null; initial: RankFo
export function RanksTab() {
const trpc = useTRPC();
const qc = useQueryClient();
const [editor, setEditor] = useState<EditorState>(null);
const { data: ranks = [], isLoading } = useQuery(trpc.referralAdmin.listRanks.queryOptions());
function onSaved() {
toast.success("Gespeichert. Ränge aller Nutzer wurden neu berechnet.");
setEditor(null);
void qc.invalidateQueries(trpc.referralAdmin.listRanks.queryFilter());
void qc.invalidateQueries(trpc.referrals.ranks.queryFilter());
}
const onError = (err: { message: string }) => toast.error(err.message);
@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMutation, useQuery } from "@tanstack/react-query";
import { Loader2 } from "lucide-react";
import { toast } from "sonner";
import { useTRPC } from "@/trpc/client";
@@ -25,7 +25,6 @@ export function SettingsTab() {
function SettingsForm({ initial }: { initial: SettingsValues }) {
const trpc = useTRPC();
const qc = useQueryClient();
const [form, setForm] = useState<SettingsValues>({
programActive: initial.programActive,
redeemWindowDays: initial.redeemWindowDays,
@@ -38,7 +37,6 @@ function SettingsForm({ initial }: { initial: SettingsValues }) {
trpc.referralAdmin.updateSettings.mutationOptions({
onSuccess: () => {
toast.success("Einstellungen gespeichert");
void qc.invalidateQueries(trpc.referralAdmin.getSettings.queryFilter());
},
onError: (err) => toast.error(err.message),
})
@@ -2,7 +2,7 @@
import { useState } from "react";
import { toast } from "sonner";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMutation, useQuery } from "@tanstack/react-query";
import { Loader2 } from "lucide-react";
import { useTRPC } from "@/trpc/client";
import { Button } from "@/components/ui/button";
@@ -32,7 +32,6 @@ interface UserActionDialogsProps {
*/
export function UserActionDialogs({ target, onClose }: UserActionDialogsProps) {
const trpc = useTRPC();
const qc = useQueryClient();
const [reason, setReason] = useState("");
const [banUntil, setBanUntil] = useState("");
const [confirmation, setConfirmation] = useState("");
@@ -45,10 +44,8 @@ export function UserActionDialogs({ target, onClose }: UserActionDialogsProps) {
setConfirmation("");
onClose();
};
const refresh = () => void qc.invalidateQueries({ queryKey: trpc.admin.listUsers.queryKey() });
const failed = (label: string) => (err: { message: string }) => toast.error(`${label} fehlgeschlagen: ${err.message}`);
const done = (message: string) => () => {
refresh();
toast.success(message);
close();
};
@@ -61,8 +58,6 @@ export function UserActionDialogs({ target, onClose }: UserActionDialogsProps) {
const warn = useMutation(
trpc.admin.issueWarning.mutationOptions({
onSuccess: () => {
refresh();
void qc.invalidateQueries({ queryKey: trpc.admin.listWarnings.queryKey({ ownerId }) });
setReason("");
toast.success("Verwarnung gespeichert.");
},
-2
View File
@@ -434,7 +434,6 @@ export const adminRouter = router({
.input(z.object({ postId: z.string().min(1), reason: z.string().max(500).optional() }))
.mutation(async ({ ctx, input }) => {
const callerRole = await assertAdmin(ctx.prisma, ctx.userId, AdminRoleType.MODERATOR);
return deletePostAsModerator(ctx, callerRole, input.postId, input.reason, false);
}),
@@ -1319,7 +1318,6 @@ export const adminRouter = router({
)
.mutation(async ({ ctx, input }) => {
const callerRole = await assertAdmin(ctx.prisma, ctx.userId, AdminRoleType.MODERATOR);
return deletePostAsModerator(ctx, callerRole, input.postId, input.reason, input.notifyReporters);
}),