Implement creator studio and upload updates

This commit is contained in:
2026-04-04 10:12:02 +02:00
parent 1da7d3bf88
commit 0b216b7ecd
15107 changed files with 31206 additions and 626514 deletions

View File

@@ -0,0 +1,178 @@
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>
)
}