From 0f34a2c9c6c1ba408618fe1c40cddab7aebb7e5e Mon Sep 17 00:00:00 2001 From: neodk2004 Date: Sun, 21 Jun 2026 16:10:01 +0200 Subject: [PATCH] =?UTF-8?q?feat(admin):=20show=20post=20preview=20in=20rep?= =?UTF-8?q?orts=20=E2=80=94=20images,=20caption,=20pet,=20counts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/p/[secret]/reports/page.tsx | 179 ++++++++++++++++++++-------- src/trpc/routers/admin.ts | 36 +++++- 2 files changed, 165 insertions(+), 50 deletions(-) diff --git a/src/app/p/[secret]/reports/page.tsx b/src/app/p/[secret]/reports/page.tsx index ce3889a..607a57d 100644 --- a/src/app/p/[secret]/reports/page.tsx +++ b/src/app/p/[secret]/reports/page.tsx @@ -3,7 +3,8 @@ import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useTRPC } from "@/trpc/client"; -import { Flag, Trash2, X, Loader2, CheckCheck } from "lucide-react"; +import { getMediaUrl } from "@/lib/media-url"; +import { Flag, Trash2, X, Loader2, CheckCheck, MessageCircle, Heart, ImageOff } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; @@ -79,71 +80,151 @@ export default function AdminReportsPage() {

Keine offenen Reports — alles sauber.

) : ( -
+
{reports.map((report) => (
- + {/* Report header */} +
+ -
-
- - {REASON_LABELS[report.reason] ?? report.reason} - - - von {report.reporter.name} - - - {new Date(report.createdAt).toLocaleDateString("de-DE")} - -
- - {report.notes && ( -

{report.notes}

- )} - -
- {report.targetPostId && ( - Post: {report.targetPostId} - )} - {report.targetPetId && ( - Pet: {report.targetPetId} +
+
+ + {REASON_LABELS[report.reason] ?? report.reason} + + + gemeldet von{" "} + {report.reporter.name} + + + {new Date(report.createdAt).toLocaleDateString("de-DE", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + })} + +
+ {report.notes && ( +

+ “{report.notes}” +

)}
-
-
- - - {report.targetPostId && ( + {/* Actions */} +
- )} + {report.targetPostId && ( + + )} +
+ + {/* Post preview */} + {report.post ? ( +
+ {/* Image strip */} +
+ {report.post.images.length > 0 ? ( + report.post.images.map((img, i) => ( + // eslint-disable-next-line @next/next/no-img-element + + )) + ) : ( +
+ +
+ )} +
+ + {/* Post meta */} +
+
+ {/* Pet */} +
+ {report.post.pet.avatarKey ? ( + // eslint-disable-next-line @next/next/no-img-element + + ) : ( +
+ )} + + {report.post.pet.name} + + + {new Date(report.post.createdAt).toLocaleDateString("de-DE")} + +
+ + {/* Caption */} + {report.post.caption ? ( +

+ {report.post.caption} +

+ ) : ( +

Kein Caption

+ )} +
+ + {/* Counts */} +
+ + + {report.post._count.reactions} + + + + {report.post._count.comments} + +
+
+
+ ) : report.targetPetId ? ( +
+

+ Profil-Report — Pet-ID:{" "} + {report.targetPetId} +

+
+ ) : null}
))}
)} + {/* Confirm dialog */} { if (!o) closeDialog(); }}> @@ -179,7 +260,9 @@ export default function AdminReportsPage() { variant={action?.kind === "deletePost" ? "destructive" : "default"} onClick={confirm} disabled={isPending} - className={action?.kind === "dismiss" ? "bg-zinc-700 hover:bg-zinc-600 text-zinc-100" : ""} + className={ + action?.kind === "dismiss" ? "bg-zinc-700 hover:bg-zinc-600 text-zinc-100" : "" + } > {isPending && } {action?.kind === "dismiss" ? "Ignorieren" : "Post löschen"} diff --git a/src/trpc/routers/admin.ts b/src/trpc/routers/admin.ts index 2bd16bd..d92dbec 100644 --- a/src/trpc/routers/admin.ts +++ b/src/trpc/routers/admin.ts @@ -210,13 +210,45 @@ export const adminRouter = router({ // ─── Reports ───────────────────────────────────────────────────────────────── listReports: protectedProcedure.query(async ({ ctx }) => { await assertAdmin(ctx.prisma, ctx.userId); - return ctx.prisma.report.findMany({ + + const reports = await ctx.prisma.report.findMany({ include: { - reporter: { select: { id: true, name: true } }, + reporter: { select: { id: true, name: true, avatarKey: true } }, }, orderBy: { createdAt: "desc" }, take: 100, }); + + // targetPostId is a raw string — no Prisma relation, so fetch posts separately + const postIds = [...new Set( + reports.map((r) => r.targetPostId).filter((id): id is string => !!id) + )]; + + const posts = postIds.length > 0 + ? await ctx.prisma.post.findMany({ + where: { id: { in: postIds } }, + select: { + id: true, + caption: true, + type: true, + createdAt: true, + pet: { select: { id: true, name: true, avatarKey: true } }, + images: { + orderBy: { position: "asc" }, + take: 4, + select: { storageKey: true, position: true }, + }, + _count: { select: { reactions: true, comments: true } }, + }, + }) + : []; + + const postMap = new Map(posts.map((p) => [p.id, p])); + + return reports.map((r) => ({ + ...r, + post: r.targetPostId ? (postMap.get(r.targetPostId) ?? null) : null, + })); }), dismissReport: protectedProcedure