refactor: improve image handling and accessibility in MessagesPage
Deploy to VPS / deploy (push) Successful in 2m17s

This commit is contained in:
asepharyana
2026-07-26 17:31:44 +07:00
parent eca0a378f9
commit 9ba2262446
@@ -5,14 +5,14 @@ import {
ExternalLink, ExternalLink,
Flag, Flag,
Hash, Hash,
ImageIcon,
Loader2, Loader2,
MessageSquare, MessageSquare,
RefreshCw, RefreshCw,
Search, Search,
Sparkles, Sparkles,
} from "lucide-react"; } from "lucide-react";
import Image from "next/image"; import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { EmptyState, ErrorState, LoadingSkeleton } from "@/components/shared"; import { EmptyState, ErrorState, LoadingSkeleton } from "@/components/shared";
import { GuildSelector } from "@/components/shared/guild-selector"; import { GuildSelector } from "@/components/shared/guild-selector";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
@@ -247,7 +247,10 @@ export default function MessagesPage() {
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 animate-fade-in-up"> <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 animate-fade-in-up">
{!images || images.length === 0 ? ( {!images || images.length === 0 ? (
<div className="col-span-full flex flex-col items-center justify-center py-20 text-center"> <div className="col-span-full flex flex-col items-center justify-center py-20 text-center">
<ImageIcon className="size-10 text-muted-foreground/40 mb-3" /> <ImageIcon
className="size-10 text-muted-foreground/40 mb-3"
aria-label="No images"
/>
<p className="text-sm text-muted-foreground">No images yet.</p> <p className="text-sm text-muted-foreground">No images yet.</p>
</div> </div>
) : ( ) : (
@@ -261,12 +264,10 @@ export default function MessagesPage() {
> >
<div className="aspect-square relative bg-muted"> <div className="aspect-square relative bg-muted">
{imgUrl ? ( {imgUrl ? (
<Image <img
src={imgUrl} src={imgUrl}
alt={msg.content || "Image"} alt={msg.content || "Image"}
fill className="absolute inset-0 size-full object-cover transition-transform duration-300 group-hover:scale-105"
className="object-cover transition-transform duration-300 group-hover:scale-105"
sizes="(max-width: 768px) 50vw, 25vw"
/> />
) : ( ) : (
<div className="flex items-center justify-center size-full text-muted-foreground text-xs"> <div className="flex items-center justify-center size-full text-muted-foreground text-xs">
@@ -422,6 +423,17 @@ function MessageCard({
> >
{msg.content} {msg.content}
</p> </p>
{(() => {
const u = extractFirstImage(msg.metadata);
if (!u) return null;
return (
<img
src={u}
alt=""
className="mt-2 max-h-48 rounded-lg border border-border/50 object-cover"
/>
);
})()}
{msg.ai_moderation_flags && msg.ai_moderation_flags !== "[]" && ( {msg.ai_moderation_flags && msg.ai_moderation_flags !== "[]" && (
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{safeParseJsonArray(msg.ai_moderation_flags).map((f) => ( {safeParseJsonArray(msg.ai_moderation_flags).map((f) => (
@@ -651,7 +663,7 @@ function MiniStat({
// ── Helpers ────────────────────────────────────────────────────── // ── Helpers ──────────────────────────────────────────────────────
function extractImage(metadata: string | null | undefined): string | null { function extractFirstImage(metadata: string | null | undefined): string | null {
if (!metadata) return null; if (!metadata) return null;
try { try {
const m = JSON.parse(metadata); const m = JSON.parse(metadata);
@@ -663,26 +675,14 @@ function extractImage(metadata: string | null | undefined): string | null {
} }
} }
function ImageIcon({ className }: { className?: string }) { function extractImage(metadata: string | null | undefined): string | null {
return ( if (!metadata) return null;
<svg try {
xmlns="http://www.w3.org/2000/svg" const m = JSON.parse(metadata);
width="24" const atts: Array<{ url: string; contentType?: string }> =
height="24" m.attachments ?? [];
viewBox="0 0 24 24" return atts.find((a) => a.contentType?.startsWith("image/"))?.url ?? null;
fill="none" } catch {
stroke="currentColor" return null;
strokeWidth="2" }
strokeLinecap="round"
strokeLinejoin="round"
className={className}
role="img"
aria-label="Image"
>
<title>Image</title>
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
<circle cx="9" cy="9" r="2" />
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
</svg>
);
} }