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:
91
resources/js/components/artwork/ArtworkDetailsDrawer.jsx
Normal file
91
resources/js/components/artwork/ArtworkDetailsDrawer.jsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import ArtworkBreadcrumbs from './ArtworkBreadcrumbs'
|
||||
|
||||
function formatCount(value) {
|
||||
const number = Number(value || 0)
|
||||
if (number >= 1_000_000) return `${(number / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`
|
||||
if (number >= 1_000) return `${(number / 1_000).toFixed(1).replace(/\.0$/, '')}k`
|
||||
return `${number}`
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) return '—'
|
||||
try {
|
||||
return new Date(value).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
|
||||
} catch {
|
||||
return '—'
|
||||
}
|
||||
}
|
||||
|
||||
export default function ArtworkDetailsDrawer({ isOpen, onClose, artwork, stats }) {
|
||||
const width = artwork?.dimensions?.width || artwork?.width || 0
|
||||
const height = artwork?.dimensions?.height || artwork?.height || 0
|
||||
|
||||
const fileType = useMemo(() => {
|
||||
const mime = artwork?.file?.mime_type || artwork?.mime_type || ''
|
||||
if (mime) return mime
|
||||
const url = artwork?.file?.url || artwork?.thumbs?.xl?.url || ''
|
||||
const ext = url.split('.').pop()
|
||||
return ext ? ext.toUpperCase() : '—'
|
||||
}, [artwork])
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[70]">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close details"
|
||||
className="absolute inset-0 bg-black/55 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 max-h-[90vh] overflow-y-auto rounded-t-3xl border border-white/10 bg-nova-900/85 p-5 backdrop-blur xl:inset-auto xl:right-6 xl:top-24 xl:w-[34rem] xl:rounded-3xl xl:border-white/15 xl:p-6">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-base font-semibold text-white">Details</h2>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close details drawer"
|
||||
onClick={onClose}
|
||||
className="inline-flex h-9 w-9 items-center justify-center rounded-full border border-white/15 bg-white/5 text-white/80 transition hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-white/10 bg-black/15 p-4">
|
||||
<ArtworkBreadcrumbs artwork={artwork} />
|
||||
</div>
|
||||
|
||||
<dl className="mt-4 grid grid-cols-1 gap-3 text-sm sm:grid-cols-2">
|
||||
<div className="rounded-xl border border-white/10 bg-black/20 px-3 py-2.5">
|
||||
<dt className="text-soft">Resolution</dt>
|
||||
<dd className="mt-1 font-medium text-white">{width > 0 && height > 0 ? `${width} × ${height}` : '—'}</dd>
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/10 bg-black/20 px-3 py-2.5">
|
||||
<dt className="text-soft">Upload date</dt>
|
||||
<dd className="mt-1 font-medium text-white">{formatDate(artwork?.published_at)}</dd>
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/10 bg-black/20 px-3 py-2.5">
|
||||
<dt className="text-soft">File type</dt>
|
||||
<dd className="mt-1 font-medium text-white">{fileType}</dd>
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/10 bg-black/20 px-3 py-2.5">
|
||||
<dt className="text-soft">Views</dt>
|
||||
<dd className="mt-1 font-medium text-white">{formatCount(stats?.views)}</dd>
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/10 bg-black/20 px-3 py-2.5">
|
||||
<dt className="text-soft">Downloads</dt>
|
||||
<dd className="mt-1 font-medium text-white">{formatCount(stats?.downloads)}</dd>
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/10 bg-black/20 px-3 py-2.5">
|
||||
<dt className="text-soft">Favorites</dt>
|
||||
<dd className="mt-1 font-medium text-white">{formatCount(stats?.favorites)}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user