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:
1 parent
83513828c6
commit
ed6842bc39
2 files changed
+72
-51
No files matched your search
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user