Files
SkinbaseNova/resources/js/components/forum/ThreadRow.jsx

137 lines
4.8 KiB
JavaScript

import React from 'react'
export default function ThreadRow({ thread, isFirst = false }) {
const id = thread?.topic_id ?? thread?.id ?? 0
const title = thread?.topic ?? thread?.title ?? 'Untitled'
const slug = thread?.slug ?? slugify(title)
const excerpt = thread?.discuss ?? ''
const posts = thread?.num_posts ?? 0
const author = thread?.uname ?? 'Unknown'
const lastUpdate = thread?.last_update ?? thread?.post_date
const isPinned = thread?.is_pinned ?? false
const href = `/forum/topic/${slug}`
return (
<a
href={href}
className={[
'group flex items-start gap-4 px-5 py-4 transition-colors hover:bg-white/[0.03]',
!isFirst && 'border-t border-white/[0.06]',
].filter(Boolean).join(' ')}
>
{/* Icon */}
<div className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-sky-500/10 text-sky-400 group-hover:bg-sky-500/15 transition-colors">
{isPinned ? (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2z" />
</svg>
) : (
<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>
{/* Content */}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<h3 className="m-0 truncate text-sm font-semibold leading-tight text-white transition-colors group-hover:text-sky-300">
{title}
</h3>
{isPinned && (
<span className="shrink-0 rounded-full bg-amber-500/15 px-2 py-0.5 text-[10px] font-medium text-amber-300">
Pinned
</span>
)}
</div>
{excerpt && (
<p className="mt-0.5 truncate text-xs text-white/40">
{stripHtml(excerpt).slice(0, 180)}
</p>
)}
<div className="mt-1.5 flex flex-wrap items-center gap-3 text-xs text-white/35">
<span className="flex items-center gap-1">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
{author}
</span>
<span className="flex items-center gap-1">
<svg width="12" height="12" 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>
{posts} {posts === 1 ? 'reply' : 'replies'}
</span>
{lastUpdate && (
<span className="flex items-center gap-1">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
{formatDate(lastUpdate)}
</span>
)}
</div>
</div>
{/* Reply count badge */}
<div className="mt-1 shrink-0">
<span className="inline-flex min-w-[2rem] items-center justify-center rounded-full bg-white/[0.06] px-2.5 py-1 text-xs font-medium text-white/60">
{posts}
</span>
</div>
</a>
)
}
function slugify(text) {
return (text ?? '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '')
.slice(0, 80)
}
function stripHtml(html) {
const decodeEntities = (value) => {
let decoded = String(value ?? '')
for (let index = 0; index < 4; index += 1) {
if (!decoded.includes('&')) break
if (typeof document !== 'undefined') {
const textarea = document.createElement('textarea')
textarea.innerHTML = decoded
const next = textarea.value
if (next === decoded) break
decoded = next
} else {
break
}
}
return decoded
}
if (typeof document !== 'undefined') {
const div = document.createElement('div')
div.innerHTML = decodeEntities(html)
return div.textContent || div.innerText || ''
}
return decodeEntities(html).replace(/<[^>]*>/g, '')
}
function formatDate(dateStr) {
try {
const d = new Date(dateStr)
return d.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })
+ ' ' + d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })
} catch {
return '-'
}
}