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:
96
resources/js/components/Feed/LinkPreviewCard.jsx
Normal file
96
resources/js/components/Feed/LinkPreviewCard.jsx
Normal file
@@ -0,0 +1,96 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user