feat: artwork page carousels, recommendations, avatars & fixes
- Infinite loop carousels for Similar Artworks & Trending rails
- Mouse wheel horizontal scrolling on both carousels
- Author avatar shown on hover in RailCard (similar + trending)
- Removed "View" badge from RailCard hover overlay
- Added `id` to Meilisearch filterable attributes
- Auto-prepend Scout prefix in meilisearch:configure-index command
- Added author name + avatar to Similar Artworks API response
- Added avatar_url to ArtworkListResource author object
- Added direct /art/{id}/{slug} URL to ArtworkListResource
- Fixed race condition: Similar Artworks no longer briefly shows trending items
- Fixed user_profiles eager load (user_id primary key, not id)
- Bumped /api/art/{id}/similar rate limit to 300/min
- Removed decorative heart icons from tag pills
- Moved ReactionBar under artwork description
This commit is contained in:
84
resources/js/components/artwork/ArtworkDetailsPanel.jsx
Normal file
84
resources/js/components/artwork/ArtworkDetailsPanel.jsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React from 'react'
|
||||
|
||||
function formatCount(value) {
|
||||
const n = Number(value || 0)
|
||||
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`
|
||||
if (n >= 1_000) return `${(n / 1_000).toFixed(1).replace(/\.0$/, '')}k`
|
||||
return n.toLocaleString()
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) return '—'
|
||||
try {
|
||||
const d = new Date(value)
|
||||
const now = Date.now()
|
||||
const diff = now - d.getTime()
|
||||
const days = Math.floor(diff / 86_400_000)
|
||||
if (days === 0) return 'Today'
|
||||
if (days === 1) return 'Yesterday'
|
||||
if (days < 30) return `${days} days ago`
|
||||
return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
|
||||
} catch {
|
||||
return '—'
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Stat tile shown in the 2-col grid ─────────────────────────────────── */
|
||||
function StatTile({ icon, label, value }) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-1.5 rounded-xl bg-white/[0.03] px-3 py-3.5">
|
||||
<span className="text-white/30">{icon}</span>
|
||||
<span className="text-base font-semibold tabular-nums text-white/90">{value}</span>
|
||||
<span className="text-[11px] uppercase tracking-wider text-white/35">{label}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* ── Key-value row ─────────────────────────────────────────────────────── */
|
||||
function InfoRow({ label, value }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-xs uppercase tracking-wider text-white/35">{label}</span>
|
||||
<span className="text-sm font-medium text-white/80">{value}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ArtworkDetailsPanel({ artwork, stats }) {
|
||||
const width = artwork?.dimensions?.width || artwork?.width || 0
|
||||
const height = artwork?.dimensions?.height || artwork?.height || 0
|
||||
const resolution = width > 0 && height > 0 ? `${width.toLocaleString()} × ${height.toLocaleString()}` : null
|
||||
|
||||
return (
|
||||
<section className="rounded-2xl border border-white/[0.06] bg-white/[0.03] p-5">
|
||||
{/* Stats grid */}
|
||||
<div className="grid grid-cols-2 gap-2.5">
|
||||
<StatTile
|
||||
icon={
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="h-4.5 w-4.5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
}
|
||||
label="Views"
|
||||
value={formatCount(stats?.views)}
|
||||
/>
|
||||
<StatTile
|
||||
icon={
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="h-4.5 w-4.5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
}
|
||||
label="Downloads"
|
||||
value={formatCount(stats?.downloads)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Info rows */}
|
||||
<div className="mt-4 divide-y divide-white/[0.05]">
|
||||
{resolution && <InfoRow label="Resolution" value={resolution} />}
|
||||
<InfoRow label="Uploaded" value={formatDate(artwork?.published_at)} />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user