178 lines
8.5 KiB
JavaScript
178 lines
8.5 KiB
JavaScript
import React, { useEffect, useMemo, useState } from 'react'
|
|
import { router, usePage } from '@inertiajs/react'
|
|
import StudioLayout from '../../Layouts/StudioLayout'
|
|
|
|
const modules = [
|
|
{ key: 'artworks', label: 'Artworks' },
|
|
{ key: 'cards', label: 'Cards' },
|
|
{ key: 'collections', label: 'Collections' },
|
|
{ key: 'stories', label: 'Stories' },
|
|
]
|
|
|
|
async function requestJson(url, method, 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) {
|
|
throw new Error(payload?.message || payload?.error || 'Request failed')
|
|
}
|
|
|
|
return payload
|
|
}
|
|
|
|
export default function StudioFeatured() {
|
|
const { props } = usePage()
|
|
const [featuredModules, setFeaturedModules] = useState(props.featuredModules || [])
|
|
const [selected, setSelected] = useState(props.selected || {})
|
|
const [saving, setSaving] = useState(false)
|
|
|
|
useEffect(() => {
|
|
setFeaturedModules(props.featuredModules || [])
|
|
setSelected(props.selected || {})
|
|
}, [props.featuredModules, props.selected])
|
|
|
|
const groupedItems = useMemo(() => {
|
|
return (props.items || []).reduce((accumulator, item) => {
|
|
const key = item.module || 'unknown'
|
|
accumulator[key] = [...(accumulator[key] || []), item]
|
|
return accumulator
|
|
}, {})
|
|
}, [props.items])
|
|
|
|
const toggleModule = (module) => {
|
|
setFeaturedModules((current) => (
|
|
current.includes(module)
|
|
? current.filter((entry) => entry !== module)
|
|
: [...current, module]
|
|
))
|
|
}
|
|
|
|
const saveSelections = async () => {
|
|
setSaving(true)
|
|
|
|
try {
|
|
await requestJson(props.endpoints.save, 'PUT', {
|
|
featured_modules: featuredModules,
|
|
featured_content: selected,
|
|
})
|
|
|
|
router.reload({ preserveScroll: true, preserveState: true })
|
|
} catch (error) {
|
|
window.alert(error?.message || 'Unable to save featured content.')
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<StudioLayout title={props.title} subtitle={props.description}>
|
|
<section className="mb-6 rounded-[28px] border border-sky-300/20 bg-sky-300/10 p-5 text-sky-100">
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.2em]">Profile highlights</p>
|
|
<p className="mt-2 max-w-3xl text-sm leading-6">Choose which modules are highlighted on your profile, then assign one representative item to each active module.</p>
|
|
<button type="button" onClick={saveSelections} disabled={saving} className="mt-4 inline-flex items-center gap-2 rounded-full border border-sky-300/20 px-4 py-2 text-sm font-semibold disabled:opacity-50">
|
|
<i className="fa-solid fa-floppy-disk" />
|
|
{saving ? 'Saving...' : 'Save featured layout'}
|
|
</button>
|
|
</section>
|
|
|
|
<section className="mb-6 rounded-[28px] border border-white/10 bg-white/[0.03] p-6">
|
|
<h2 className="text-lg font-semibold text-white">Active modules</h2>
|
|
<div className="mt-4 flex flex-wrap gap-3">
|
|
{modules.map((module) => {
|
|
const active = featuredModules.includes(module.key)
|
|
|
|
return (
|
|
<button
|
|
key={module.key}
|
|
type="button"
|
|
onClick={() => toggleModule(module.key)}
|
|
className={`inline-flex items-center gap-2 rounded-full border px-4 py-2 text-sm font-medium transition ${active ? 'border-sky-300/25 bg-sky-300/10 text-sky-100' : 'border-white/10 bg-black/20 text-slate-300'}`}
|
|
>
|
|
<i className={`fa-solid ${active ? 'fa-circle-check' : 'fa-circle'}`} />
|
|
{module.label}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
</section>
|
|
|
|
<div className="space-y-6">
|
|
{modules.map((module) => {
|
|
const items = groupedItems[module.key] || []
|
|
const active = featuredModules.includes(module.key)
|
|
|
|
return (
|
|
<section key={module.key} className="rounded-[28px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-white">{module.label}</h2>
|
|
<p className="mt-1 text-sm text-slate-400">Select one featured item that represents this module on your profile.</p>
|
|
</div>
|
|
<span className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-[0.16em] ${active ? 'bg-sky-300/10 text-sky-100' : 'bg-white/5 text-slate-500'}`}>
|
|
{active ? 'Active' : 'Hidden'}
|
|
</span>
|
|
</div>
|
|
|
|
{items.length > 0 ? (
|
|
<div className="mt-5 grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
|
|
{items.map((item) => {
|
|
const isSelected = Number(selected[module.key] || 0) === Number(item.numeric_id || 0)
|
|
|
|
return (
|
|
<article key={item.id} className={`overflow-hidden rounded-[28px] border ${isSelected ? 'border-sky-300/30 bg-sky-300/5' : 'border-white/10 bg-white/[0.02]'}`}>
|
|
<div className="aspect-[1.15/1] bg-slate-950/70">
|
|
{item.image_url ? (
|
|
<img src={item.image_url} alt={item.title} className="h-full w-full object-cover" loading="lazy" />
|
|
) : (
|
|
<div className="flex h-full items-center justify-center text-slate-500">
|
|
<i className={item.module_icon || 'fa-solid fa-star'} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="space-y-3 p-5">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/70">{item.module_label}</p>
|
|
<h3 className="truncate text-lg font-semibold text-white">{item.title}</h3>
|
|
<p className="text-sm text-slate-400">{item.subtitle || item.visibility || 'Published item'}</p>
|
|
</div>
|
|
<button type="button" onClick={() => setSelected((current) => ({ ...current, [module.key]: item.numeric_id }))} className={`inline-flex h-10 w-10 items-center justify-center rounded-full border ${isSelected ? 'border-sky-300/30 bg-sky-300/10 text-sky-100' : 'border-white/10 bg-black/20 text-slate-400'}`}>
|
|
<i className={`fa-solid ${isSelected ? 'fa-check' : 'fa-star'}`} />
|
|
</button>
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-2 text-xs text-slate-400">
|
|
<div><div>Views</div><div className="mt-1 text-sm font-semibold text-white">{Number(item.metrics?.views || 0).toLocaleString()}</div></div>
|
|
<div><div>Reactions</div><div className="mt-1 text-sm font-semibold text-white">{Number(item.metrics?.appreciation || 0).toLocaleString()}</div></div>
|
|
<div><div>Comments</div><div className="mt-1 text-sm font-semibold text-white">{Number(item.metrics?.comments || 0).toLocaleString()}</div></div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<a href={item.edit_url || item.manage_url} className="inline-flex items-center gap-2 rounded-full border border-white/10 px-3 py-1.5 text-xs text-slate-100">Edit</a>
|
|
<a href={item.preview_url || item.view_url} className="inline-flex items-center gap-2 rounded-full border border-white/10 px-3 py-1.5 text-xs text-slate-100">Preview</a>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
)
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="mt-5 rounded-[24px] border border-dashed border-white/15 px-6 py-10 text-center text-sm text-slate-400">
|
|
No published {module.label.toLowerCase()} candidates yet.
|
|
</div>
|
|
)}
|
|
</section>
|
|
)
|
|
})}
|
|
</div>
|
|
</StudioLayout>
|
|
)
|
|
} |