forum: image attachments on threads + replies, AVIF picture render

New ImageAttachButton drops onto any composer textarea: file picker
upload → /api/upload/image (the universal pipeline) → markdown
![alt](primaryUrl) appended to the body. New thread + reply composers
both gain the button with a 'Auto-optimized, never > 20 MB' caption.

RichBody is the read-path companion: parses body markdown for image
syntax and renders each as a <picture> element with the AVIF source
first (derived from the .webp sibling on our bb-media bucket), then the
WebP, then the original-format fallback img. Prose around images still
runs through LinkifyPlainText so company auto-links still work.

Forum users now have a way to include screenshots, gear photos, NAB
booth shots, etc. in their posts — every image flows through the same
optimization that gives <10% of original byte weight at visually
identical quality.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ryan Salazar
2026-05-29 16:08:59 +00:00
parent e24d78cd15
commit 39e9777e0a
4 changed files with 187 additions and 2 deletions

View File

@@ -4,6 +4,7 @@ import Link from 'next/link';
import { useParams } from 'next/navigation';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import ImageAttachButton from '@/components/forum/ImageAttachButton';
import { createClient } from '@/lib/supabase/client';
interface ForumCategory {
@@ -336,6 +337,10 @@ export default function ForumCategoryPage() {
rows={5}
className="w-full bg-[#111] border border-[#333] rounded-lg px-3 py-2 text-white font-body text-sm placeholder-[#555] focus:outline-none focus:border-[#3b82f6] resize-none"
/>
<div className="flex items-center gap-3">
<ImageAttachButton onInsert={(md) => setNewBody((b) => (b || "") + md)} />
<span className="text-[10px] text-[#666]">Auto-optimized, never &gt; 20 MB</span>
</div>
{newError && (
<div className="bg-[#2a0a0a] border border-[#cc0000] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded">
{newError}

View File

@@ -9,6 +9,8 @@ import { pickAds, rotateAll } from '@/lib/ads';
import { createClient } from '@/lib/supabase/client';
import SidebarAdStack from "@/components/SidebarAdStack";
import LinkifyPlainText from "@/components/LinkifyPlainText";
import RichBody from "@/components/forum/RichBody";
import ImageAttachButton from "@/components/forum/ImageAttachButton";
import CompanyMentionsHover from "@/components/CompanyMentionsHover";
// Inline ad slot inserted between forum replies every N posts.
@@ -371,7 +373,7 @@ export default function ForumThreadPage() {
<span className="text-[#555] font-body text-xs">{thread.view_count} views</span>
</div>
<p className="text-[#d0d0d0] font-body text-sm leading-relaxed">
<LinkifyPlainText body={thread.body} />
<RichBody body={thread.body} />
</p>
<VoteButtons
targetType="thread"
@@ -413,7 +415,7 @@ export default function ForumThreadPage() {
<span className="text-[#555] font-body text-xs">{timeAgo(reply.created_at)}</span>
</div>
<p className="text-[#d0d0d0] font-body text-sm leading-relaxed">
<LinkifyPlainText body={reply.body} />
<RichBody body={reply.body} />
</p>
<VoteButtons
targetType="reply"
@@ -495,6 +497,10 @@ export default function ForumThreadPage() {
rows={5}
className="w-full bg-[#111] border border-[#333] rounded-lg px-3 py-2 text-white font-body text-sm placeholder-[#555] focus:outline-none focus:border-[#3b82f6] resize-none"
/>
<div className="flex items-center gap-3">
<ImageAttachButton onInsert={(md) => setReplyBody((b) => (b || "") + md)} />
<span className="text-[10px] text-[#666]">Auto-optimized, never &gt; 20 MB</span>
</div>
{replyError && (
<div className="bg-[#2a0a0a] border border-[#cc0000] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded">
{replyError}