137 lines
4.8 KiB
JavaScript
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 '-'
|
|
}
|
|
}
|