feat(admin): redesign Posts panel as a 3-column media grid (Gitea #7)

The list view made post content hard to scan — thumbnails rendered at
inconsistent sizes and captions were easy to miss. Switches to a
responsive grid (3 cols on desktop) with a fixed-size thumbnail per
card, a same-size placeholder for posts without media, a caption
below each card, and a play-icon overlay + Mux thumbnail for video
posts (listPosts now also selects videoPost.muxPlaybackId).
This commit is contained in:
admin committed 2026-08-11 14:47:06 +02:00
1 parent 83513828c6
commit ed6842bc39
2 files changed
+72 -51

No files matched your search

+71 -51
View File
@@ -3,7 +3,7 @@
import { useState } from "react";
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useTRPC } from "@/trpc/client";
import { Trash2, MessageSquare, Loader2, ChevronDown } from "lucide-react";
import { Trash2, MessageSquare, Loader2, ChevronDown, Play, ImageOff } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
@@ -82,58 +82,78 @@ export default function AdminPostsPage() {
<Loader2 className="h-5 w-5 animate-spin text-zinc-500" />
</div>
) : (
<div className="space-y-2">
{posts.map((post) => {
const thumb = post.images[0];
return (
<div
key={post.id}
className="flex items-center gap-4 rounded-xl border border-zinc-800 bg-zinc-900 px-4 py-3"
>
{thumb ? (
<BlurImage
src={getMediaUrl(thumb.storageKey) ?? ""}
blurhash={thumb.blurhash}
alt=""
className="h-12 w-12 rounded-lg object-cover shrink-0"
/>
) : (
<div className="h-12 w-12 rounded-lg bg-zinc-800 shrink-0" />
)}
<div className="flex-1 min-w-0">
<p className="text-sm text-zinc-100 truncate">
{post.caption ?? <em className="text-zinc-500">No caption</em>}
</p>
<p className="text-xs text-zinc-500">
{post.pet.name} · {post.reactionCount} 🐾 · {post.commentCount} 💬
{post.reportCount > 0 && (
<span className="ml-2 text-yellow-400">
⚑ {post.reportCount} report{post.reportCount !== 1 ? "s" : ""}
</span>
<div className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{posts.map((post) => {
const thumb = post.images[0];
const isVideo = post.type === "VIDEO";
const muxPlaybackId = post.videoPost?.muxPlaybackId;
const mediaUrl = thumb
? getMediaUrl(thumb.storageKey)
: isVideo && muxPlaybackId
? `https://image.mux.com/${muxPlaybackId}/thumbnail.jpg?width=480&time=0`
: null;
return (
<div
key={post.id}
className="flex flex-col rounded-xl border border-zinc-800 bg-zinc-900 overflow-hidden"
>
<div className="relative aspect-square bg-zinc-800 shrink-0">
{mediaUrl ? (
<BlurImage
src={mediaUrl}
blurhash={thumb?.blurhash}
alt={post.caption ?? "Post"}
className="h-full w-full object-cover"
/>
) : (
<div className="flex h-full w-full items-center justify-center">
<ImageOff className="h-8 w-8 text-zinc-600" aria-hidden="true" />
</div>
)}
</p>
{isVideo && (
<div className="absolute top-2 right-2 rounded-full bg-black/50 p-1">
<Play className="h-3.5 w-3.5 text-white fill-white" aria-hidden="true" />
</div>
)}
</div>
<div className="flex flex-1 flex-col gap-2 p-3">
<p className="text-sm text-zinc-100 line-clamp-2 min-h-[2.5rem]">
{post.caption ?? <em className="text-zinc-500">No caption</em>}
</p>
<p className="text-xs text-zinc-500">
{post.pet.name} · {post.reactionCount} 🐾 · {post.commentCount} 💬
{post.reportCount > 0 && (
<span className="ml-2 text-yellow-400">
⚑ {post.reportCount} report{post.reportCount !== 1 ? "s" : ""}
</span>
)}
</p>
<div className="mt-auto flex justify-end gap-2 pt-1">
<Button
size="sm"
variant="ghost"
className="text-zinc-400 hover:text-zinc-100"
onClick={() => setCommentsPostId(post.id)}
>
<MessageSquare className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
className="text-red-400 hover:text-red-300"
onClick={() => setDeleteTarget({ id: post.id, type: "post" })}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</div>
<div className="flex gap-2 shrink-0">
<Button
size="sm"
variant="ghost"
className="text-zinc-400 hover:text-zinc-100"
onClick={() => setCommentsPostId(post.id)}
>
<MessageSquare className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
className="text-red-400 hover:text-red-300"
onClick={() => setDeleteTarget({ id: post.id, type: "post" })}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
);
})}
);
})}
</div>
{hasNextPage && (
<div className="flex justify-center pt-2">
+1
View File
@@ -166,6 +166,7 @@ export const adminRouter = router({
include: {
pet: { select: { id: true, name: true, avatarKey: true, ownerId: true } },
images: { orderBy: { position: "asc" }, take: 1 },
videoPost: { select: { muxPlaybackId: true } },
},
orderBy: { createdAt: "desc" },
take: input.limit + 1,