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
97 lines
3.3 KiB
JavaScript
97 lines
3.3 KiB
JavaScript
import React from 'react'
|
|
|
|
/**
|
|
* LinkPreviewCard
|
|
* Renders an OG/OpenGraph link preview card.
|
|
*
|
|
* Props:
|
|
* preview { url, title, description, image, site_name }
|
|
* onDismiss function|null — if provided, shows a dismiss ✕ button
|
|
* loading boolean — shows skeleton while fetching
|
|
*/
|
|
export default function LinkPreviewCard({ preview, onDismiss, loading = false }) {
|
|
if (loading) {
|
|
return (
|
|
<div className="rounded-xl border border-white/[0.08] bg-white/[0.03] overflow-hidden flex gap-3 p-3 animate-pulse">
|
|
<div className="w-20 h-20 rounded-lg bg-white/10 shrink-0" />
|
|
<div className="flex-1 min-w-0 flex flex-col gap-2 justify-center">
|
|
<div className="h-3 bg-white/10 rounded w-2/3" />
|
|
<div className="h-2.5 bg-white/10 rounded w-full" />
|
|
<div className="h-2.5 bg-white/10 rounded w-4/5" />
|
|
<div className="h-2 bg-white/[0.06] rounded w-1/3 mt-1" />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!preview?.url) return null
|
|
|
|
const domain = (() => {
|
|
try { return new URL(preview.url).hostname.replace(/^www\./, '') }
|
|
catch { return preview.site_name ?? '' }
|
|
})()
|
|
|
|
return (
|
|
<div className="relative rounded-xl border border-white/[0.08] bg-white/[0.03] overflow-hidden hover:border-white/[0.14] transition-colors group">
|
|
<a
|
|
href={preview.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer nofollow"
|
|
className="flex gap-0 items-stretch"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{/* Image */}
|
|
{preview.image ? (
|
|
<div className="w-24 shrink-0 bg-white/5">
|
|
<img
|
|
src={preview.image}
|
|
alt=""
|
|
className="w-full h-full object-cover"
|
|
loading="lazy"
|
|
onError={(e) => { e.currentTarget.parentElement.style.display = 'none' }}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="w-24 shrink-0 bg-white/[0.04] flex items-center justify-center text-slate-600">
|
|
<i className="fa-solid fa-link text-xl" />
|
|
</div>
|
|
)}
|
|
|
|
{/* Text */}
|
|
<div className="flex-1 min-w-0 px-3 py-2.5 flex flex-col justify-center gap-0.5">
|
|
{preview.site_name && (
|
|
<p className="text-[10px] uppercase tracking-wide text-sky-500/80 font-medium truncate">
|
|
{preview.site_name}
|
|
</p>
|
|
)}
|
|
{preview.title && (
|
|
<p className="text-sm font-semibold text-white/90 line-clamp-2 leading-snug">
|
|
{preview.title}
|
|
</p>
|
|
)}
|
|
{preview.description && (
|
|
<p className="text-xs text-slate-500 line-clamp-2 leading-relaxed mt-0.5">
|
|
{preview.description}
|
|
</p>
|
|
)}
|
|
<p className="text-[10px] text-slate-600 mt-1 truncate">
|
|
{domain}
|
|
</p>
|
|
</div>
|
|
</a>
|
|
|
|
{/* Dismiss button */}
|
|
{onDismiss && (
|
|
<button
|
|
type="button"
|
|
onClick={(e) => { e.stopPropagation(); onDismiss() }}
|
|
className="absolute top-1.5 right-1.5 w-5 h-5 rounded-full bg-black/60 hover:bg-black/80 text-slate-400 hover:text-white flex items-center justify-center transition-colors text-[10px]"
|
|
aria-label="Remove link preview"
|
|
>
|
|
<i className="fa-solid fa-xmark" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|