optimizations
This commit is contained in:
277
resources/js/components/profile/collections/CollectionCard.jsx
Normal file
277
resources/js/components/profile/collections/CollectionCard.jsx
Normal file
@@ -0,0 +1,277 @@
|
||||
import React from 'react'
|
||||
import CollectionVisibilityBadge from './CollectionVisibilityBadge'
|
||||
|
||||
async function requestJson(url, { method = 'GET', body } = {}) {
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
})
|
||||
|
||||
const payload = await response.json().catch(() => ({}))
|
||||
if (!response.ok) {
|
||||
const message = payload?.message || 'Request failed.'
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
function formatUpdated(value) {
|
||||
if (!value) return 'Updated recently'
|
||||
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return 'Updated recently'
|
||||
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
function StatPill({ icon, label, value }) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-1 text-[11px] font-medium text-slate-300">
|
||||
<i className={`fa-solid ${icon} text-[10px] text-slate-400`} />
|
||||
<span className="text-white">{value}</span>
|
||||
<span>{label}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default function CollectionCard({ collection, isOwner, onDelete, onToggleFeature, onMoveUp, onMoveDown, canMoveUp, canMoveDown, busy = false, saveContext = null, saveContextMeta = null }) {
|
||||
const coverImage = collection?.cover_image
|
||||
const [saved, setSaved] = React.useState(Boolean(collection?.saved))
|
||||
const [saveBusy, setSaveBusy] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
setSaved(Boolean(collection?.saved))
|
||||
setSaveBusy(false)
|
||||
}, [collection?.id, collection?.saved])
|
||||
|
||||
function handleDelete(event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
onDelete?.(collection)
|
||||
}
|
||||
|
||||
function stop(event) {
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
function handleToggleFeature(event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
onToggleFeature?.(collection)
|
||||
}
|
||||
|
||||
async function handleSaveToggle(event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
if (saveBusy) return
|
||||
|
||||
const targetUrl = saved ? collection?.unsave_url : collection?.save_url
|
||||
if (!targetUrl) {
|
||||
if (collection?.login_url) {
|
||||
window.location.assign(collection.login_url)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
setSaveBusy(true)
|
||||
|
||||
try {
|
||||
const payload = await requestJson(targetUrl, {
|
||||
method: saved ? 'DELETE' : 'POST',
|
||||
body: saved ? undefined : {
|
||||
context: saveContext,
|
||||
context_meta: saveContextMeta || undefined,
|
||||
},
|
||||
})
|
||||
|
||||
setSaved(Boolean(payload?.saved))
|
||||
} catch (error) {
|
||||
window.console?.error?.(error)
|
||||
} finally {
|
||||
setSaveBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={collection?.url || '#'}
|
||||
className={`group relative overflow-hidden rounded-[28px] border border-white/10 bg-white/[0.04] shadow-[0_22px_60px_rgba(2,6,23,0.22)] transition-all duration-200 hover:-translate-y-1 hover:border-sky-300/30 hover:bg-white/[0.06] ${busy ? 'opacity-70' : ''} lg:max-w-[360px] lg:mx-auto`}
|
||||
>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.12),transparent_34%),radial-gradient(circle_at_bottom_right,rgba(249,115,22,0.12),transparent_28%)] opacity-0 transition duration-300 group-hover:opacity-100" />
|
||||
<div className="relative">
|
||||
{coverImage ? (
|
||||
<div className="aspect-[16/10] overflow-hidden bg-slate-950">
|
||||
<img
|
||||
src={coverImage}
|
||||
alt={collection?.title || 'Collection cover'}
|
||||
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.04]"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex aspect-[16/10] items-center justify-center bg-[linear-gradient(135deg,rgba(8,17,31,1),rgba(15,23,42,1),rgba(8,17,31,1))] text-slate-500">
|
||||
<i className="fa-solid fa-layer-group text-4xl" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-5">
|
||||
<div className="mb-3 flex flex-wrap items-center gap-2">
|
||||
{collection?.is_featured ? (
|
||||
<span className="inline-flex items-center rounded-full border border-amber-300/25 bg-amber-300/10 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-100">
|
||||
Featured
|
||||
</span>
|
||||
) : null}
|
||||
{collection?.mode === 'smart' ? (
|
||||
<span className="inline-flex items-center rounded-full border border-sky-300/25 bg-sky-400/10 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100">
|
||||
Smart
|
||||
</span>
|
||||
) : null}
|
||||
{!isOwner && collection?.program_key ? (
|
||||
<span className="inline-flex items-center rounded-full border border-lime-300/25 bg-lime-400/10 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-lime-100">
|
||||
Program · {collection.program_key}
|
||||
</span>
|
||||
) : null}
|
||||
{!isOwner && collection?.partner_label ? (
|
||||
<span className="inline-flex items-center rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-200">
|
||||
Partner · {collection.partner_label}
|
||||
</span>
|
||||
) : null}
|
||||
{!isOwner && collection?.sponsorship_label ? (
|
||||
<span className="inline-flex items-center rounded-full border border-amber-300/20 bg-amber-300/10 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-100">
|
||||
Sponsor · {collection.sponsorship_label}
|
||||
</span>
|
||||
) : null}
|
||||
{isOwner ? <CollectionVisibilityBadge visibility={collection?.visibility} /> : null}
|
||||
</div>
|
||||
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h3 className="truncate text-lg font-semibold tracking-[-0.03em] text-white">{collection?.title}</h3>
|
||||
{collection?.subtitle ? <p className="mt-1 truncate text-sm text-slate-300">{collection.subtitle}</p> : null}
|
||||
{collection?.owner?.name ? <p className="mt-1 truncate text-sm text-slate-400">Curated by {collection.owner.name}{collection?.owner?.username ? ` • @${collection.owner.username}` : ''}</p> : null}
|
||||
<p className="mt-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">
|
||||
{(collection?.artworks_count ?? 0).toLocaleString()} artworks
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{collection?.description_excerpt ? (
|
||||
<p className="mt-3 line-clamp-2 text-sm leading-relaxed text-slate-300">{collection.description_excerpt}</p>
|
||||
) : collection?.smart_summary ? (
|
||||
<p className="mt-3 line-clamp-2 text-sm leading-relaxed text-slate-300">{collection.smart_summary}</p>
|
||||
) : null}
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
<StatPill icon="fa-heart" label="likes" value={(collection?.likes_count ?? 0).toLocaleString()} />
|
||||
<StatPill icon="fa-bell" label="followers" value={(collection?.followers_count ?? 0).toLocaleString()} />
|
||||
{collection?.collaborators_count > 1 ? <StatPill icon="fa-user-group" label="curators" value={(collection?.collaborators_count ?? 0).toLocaleString()} /> : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between gap-3 text-xs text-slate-400">
|
||||
<span>{collection?.is_featured ? 'Featured' : 'Updated'} {formatUpdated(collection?.featured_at || collection?.updated_at)}</span>
|
||||
<div className="flex items-center gap-2" onClick={stop}>
|
||||
{!isOwner && (collection?.save_url || collection?.unsave_url || collection?.login_url) ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSaveToggle}
|
||||
disabled={saveBusy}
|
||||
className={`inline-flex items-center gap-2 rounded-full border px-3 py-2 text-[11px] font-semibold uppercase tracking-[0.14em] transition ${saved ? 'border-violet-300/20 bg-violet-400/10 text-violet-100 hover:bg-violet-400/15' : 'border-white/10 bg-white/[0.04] text-white hover:bg-white/[0.08]'} disabled:opacity-60`}
|
||||
>
|
||||
<i className={`fa-solid ${saveBusy ? 'fa-circle-notch fa-spin' : (saved ? 'fa-bookmark' : 'fa-bookmark')} text-[11px]`} />
|
||||
{saved ? 'Saved' : 'Save'}
|
||||
</button>
|
||||
) : null}
|
||||
<span className="inline-flex items-center gap-1 text-slate-200">
|
||||
<i className="fa-solid fa-arrow-right text-[11px]" />
|
||||
Open
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isOwner ? (
|
||||
<div className="mt-4 flex flex-wrap gap-2" onClick={stop}>
|
||||
{collection?.visibility === 'public' ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggleFeature}
|
||||
className={`inline-flex items-center gap-2 rounded-xl border px-3 py-2 text-xs font-semibold uppercase tracking-[0.14em] transition ${collection?.is_featured ? 'border-amber-300/25 bg-amber-300/10 text-amber-100 hover:bg-amber-300/15' : 'border-sky-300/20 bg-sky-400/10 text-sky-100 hover:bg-sky-400/15'}`}
|
||||
>
|
||||
<i className={`fa-solid ${collection?.is_featured ? 'fa-star' : 'fa-sparkles'} fa-fw`} />
|
||||
{collection?.is_featured ? 'Featured' : 'Feature'}
|
||||
</button>
|
||||
) : null}
|
||||
<a
|
||||
href={collection?.edit_url || collection?.manage_url || '#'}
|
||||
className="inline-flex items-center gap-2 rounded-xl border border-white/12 bg-white/[0.05] px-3 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-slate-100 transition hover:bg-white/[0.09]"
|
||||
>
|
||||
<i className="fa-solid fa-pen-to-square fa-fw" />
|
||||
Edit
|
||||
</a>
|
||||
<a
|
||||
href={collection?.manage_url || collection?.edit_url || '#'}
|
||||
className="inline-flex items-center gap-2 rounded-xl border border-sky-300/20 bg-sky-400/10 px-3 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-sky-100 transition hover:bg-sky-400/15"
|
||||
>
|
||||
<i className="fa-solid fa-grip fa-fw" />
|
||||
Manage Artworks
|
||||
</a>
|
||||
{onMoveUp ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canMoveUp}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
onMoveUp(collection)
|
||||
}}
|
||||
className={`inline-flex items-center gap-2 rounded-xl border px-3 py-2 text-xs font-semibold uppercase tracking-[0.14em] ${canMoveUp ? 'border-white/12 bg-white/[0.05] text-white hover:bg-white/[0.09]' : 'border-white/8 bg-white/[0.03] text-slate-500'}`}
|
||||
>
|
||||
<i className="fa-solid fa-arrow-up fa-fw" />
|
||||
Up
|
||||
</button>
|
||||
) : null}
|
||||
{onMoveDown ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canMoveDown}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
onMoveDown(collection)
|
||||
}}
|
||||
className={`inline-flex items-center gap-2 rounded-xl border px-3 py-2 text-xs font-semibold uppercase tracking-[0.14em] ${canMoveDown ? 'border-white/12 bg-white/[0.05] text-white hover:bg-white/[0.09]' : 'border-white/8 bg-white/[0.03] text-slate-500'}`}
|
||||
>
|
||||
<i className="fa-solid fa-arrow-down fa-fw" />
|
||||
Down
|
||||
</button>
|
||||
) : null}
|
||||
{collection?.delete_url ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
className="inline-flex items-center gap-2 rounded-xl border border-rose-400/20 bg-rose-400/10 px-3 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-rose-100 transition hover:bg-rose-400/15"
|
||||
>
|
||||
<i className="fa-solid fa-trash-can fa-fw" />
|
||||
Delete
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user