feat: forum rich-text editor, emoji picker, mentions, discover nav, feed, uploads, profile

Forum:
- TipTap WYSIWYG editor with full toolbar
- @emoji-mart/react emoji picker (consistent with tweets)
- @mention autocomplete with user search API
- Fix PHP 8.4 parse errors in Blade templates
- Fix thread data display (paginator items)
- Align forum page widths to max-w-5xl

Discover:
- Extract shared _nav.blade.php partial
- Add missing nav links to for-you page
- Add Following link for authenticated users

Feed/Posts:
- Post model, controllers, policies, migrations
- Feed page components (PostComposer, FeedCard, etc)
- Post reactions, comments, saves, reports, sharing
- Scheduled publishing support
- Link preview controller

Profile:
- Profile page components (ProfileHero, ProfileTabs)
- Profile API controller

Uploads:
- Upload wizard enhancements
- Scheduled publish picker
- Studio status bar and readiness checklist
This commit is contained in:
2026-03-03 09:48:31 +01:00
parent 1266f81d35
commit dc51d65440
178 changed files with 14308 additions and 665 deletions

View File

@@ -0,0 +1,85 @@
import React from 'react'
export default function CategoryCard({ category }) {
const name = category?.name ?? 'Untitled'
const slug = category?.slug
const threads = category?.thread_count ?? 0
const posts = category?.post_count ?? 0
const lastActivity = category?.last_activity_at
const preview = category?.preview_image ?? '/images/forum-default.jpg'
const href = slug ? `/forum/${slug}` : '#'
const timeAgo = lastActivity ? formatTimeAgo(lastActivity) : null
return (
<a
href={href}
className="group relative block overflow-hidden rounded-2xl border border-white/[0.06] bg-nova-800/50 shadow-xl backdrop-blur transition-all duration-300 hover:border-cyan-400/20 hover:shadow-cyan-500/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400"
>
{/* Image */}
<div className="relative aspect-[16/9]">
<img
src={preview}
alt={`${name} preview`}
loading="lazy"
decoding="async"
className="h-full w-full object-cover object-center transition-transform duration-500 group-hover:scale-[1.03]"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/30 to-transparent" />
{/* Overlay content */}
<div className="absolute inset-x-0 bottom-0 p-5">
<div className="mb-2 inline-flex h-8 w-8 items-center justify-center rounded-lg bg-cyan-400/15 text-cyan-300">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
</svg>
</div>
<h3 className="text-lg font-bold text-white leading-snug">{name}</h3>
{timeAgo && (
<p className="mt-1 text-xs text-white/50">
Last activity: <span className="text-white/70">{timeAgo}</span>
</p>
)}
<div className="mt-3 flex items-center gap-4 text-sm">
<span className="flex items-center gap-1.5 text-cyan-300">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
</svg>
{number(posts)} posts
</span>
<span className="flex items-center gap-1.5 text-cyan-300/70">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
<polyline points="14 2 14 8 20 8" />
</svg>
{number(threads)} topics
</span>
</div>
</div>
</div>
</a>
)
}
function number(n) {
return (n ?? 0).toLocaleString()
}
function formatTimeAgo(dateStr) {
try {
const date = new Date(dateStr)
const now = new Date()
const diff = Math.floor((now - date) / 1000)
if (diff < 60) return 'just now'
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`
if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`
return date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })
} catch {
return null
}
}