optimizations
This commit is contained in:
196
resources/js/Pages/Collection/NovaCardsCollectionAdmin.jsx
Normal file
196
resources/js/Pages/Collection/NovaCardsCollectionAdmin.jsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import React from 'react'
|
||||
import { Head, Link, usePage } from '@inertiajs/react'
|
||||
|
||||
function requestJson(url, { method = 'GET', body } = {}) {
|
||||
return 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,
|
||||
}).then(async (response) => {
|
||||
const payload = await response.json().catch(() => ({}))
|
||||
if (!response.ok) throw new Error(payload?.message || 'Request failed')
|
||||
return payload
|
||||
})
|
||||
}
|
||||
|
||||
export default function NovaCardsCollectionAdmin() {
|
||||
const { props } = usePage()
|
||||
const [collections, setCollections] = React.useState(props.collections || [])
|
||||
const [selectedId, setSelectedId] = React.useState(props.collections?.[0]?.id || null)
|
||||
const [cardId, setCardId] = React.useState('')
|
||||
const [cardNote, setCardNote] = React.useState('')
|
||||
const endpoints = props.endpoints || {}
|
||||
const admins = props.admins || []
|
||||
const cards = props.cards || []
|
||||
|
||||
const selected = React.useMemo(() => collections.find((entry) => entry.id === selectedId) || null, [collections, selectedId])
|
||||
const [form, setForm] = React.useState(() => ({
|
||||
user_id: admins[0]?.id || '',
|
||||
slug: '',
|
||||
name: '',
|
||||
description: '',
|
||||
visibility: 'public',
|
||||
official: true,
|
||||
featured: false,
|
||||
}))
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!selected) {
|
||||
setForm({ user_id: admins[0]?.id || '', slug: '', name: '', description: '', visibility: 'public', official: true, featured: false })
|
||||
return
|
||||
}
|
||||
|
||||
setForm({
|
||||
user_id: selected.owner?.id || admins[0]?.id || '',
|
||||
slug: selected.slug || '',
|
||||
name: selected.name || '',
|
||||
description: selected.description || '',
|
||||
visibility: selected.visibility || 'public',
|
||||
official: Boolean(selected.official),
|
||||
featured: Boolean(selected.featured),
|
||||
})
|
||||
}, [admins, selected])
|
||||
|
||||
async function saveCollection() {
|
||||
const isExisting = Boolean(selectedId)
|
||||
const url = isExisting ? String(endpoints.updatePattern || '').replace('__COLLECTION__', String(selectedId)) : endpoints.store
|
||||
const response = await requestJson(url, { method: isExisting ? 'PATCH' : 'POST', body: form })
|
||||
|
||||
if (isExisting) {
|
||||
setCollections((current) => current.map((entry) => (entry.id === selectedId ? response.collection : entry)))
|
||||
} else {
|
||||
setCollections((current) => [response.collection, ...current])
|
||||
setSelectedId(response.collection.id)
|
||||
}
|
||||
}
|
||||
|
||||
async function attachCard() {
|
||||
if (!selectedId || !cardId) return
|
||||
|
||||
const response = await requestJson(String(endpoints.attachCardPattern || '').replace('__COLLECTION__', String(selectedId)), {
|
||||
method: 'POST',
|
||||
body: { card_id: Number(cardId), note: cardNote || null },
|
||||
})
|
||||
setCollections((current) => current.map((entry) => (entry.id === selectedId ? response.collection : entry)))
|
||||
setCardId('')
|
||||
setCardNote('')
|
||||
}
|
||||
|
||||
async function detachCard(collectionId, currentCardId) {
|
||||
const response = await requestJson(
|
||||
String(endpoints.detachCardPattern || '')
|
||||
.replace('__COLLECTION__', String(collectionId))
|
||||
.replace('__CARD__', String(currentCardId)),
|
||||
{ method: 'DELETE' },
|
||||
)
|
||||
setCollections((current) => current.map((entry) => (entry.id === collectionId ? response.collection : entry)))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-4 pb-20 pt-8 sm:px-6 lg:px-8">
|
||||
<Head title="Nova Cards Collections" />
|
||||
|
||||
<section className="rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_38%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.88))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)]">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.28em] text-sky-200/75">Editorial layer</p>
|
||||
<h1 className="mt-3 text-3xl font-semibold tracking-[-0.04em] text-white">Official and public card collections</h1>
|
||||
<p className="mt-3 max-w-3xl text-sm leading-7 text-slate-300">Create editorial collections, assign owners, and curate the public card sets that the v2 browse surface links to.</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button type="button" onClick={() => setSelectedId(null)} className="rounded-2xl border border-white/10 bg-white/[0.05] px-5 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]">New collection</button>
|
||||
<Link href={endpoints.cards || '/cp/cards'} className="rounded-2xl border border-white/10 bg-white/[0.05] px-5 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]">Back to cards</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="mt-8 grid gap-6 xl:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)]">
|
||||
<section className="rounded-[28px] border border-white/10 bg-white/[0.04] p-5 shadow-[0_20px_50px_rgba(2,6,23,0.18)]">
|
||||
<div className="mb-4 text-sm font-semibold uppercase tracking-[0.2em] text-slate-400">Collections</div>
|
||||
<div className="space-y-3">
|
||||
{collections.map((collection) => (
|
||||
<button key={collection.id} type="button" onClick={() => setSelectedId(collection.id)} className={`w-full rounded-[22px] border p-4 text-left transition ${selectedId === collection.id ? 'border-sky-300/35 bg-sky-400/10' : 'border-white/10 bg-white/[0.03] hover:border-white/20 hover:bg-white/[0.05]'}`}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-base font-semibold tracking-[-0.03em] text-white">{collection.name}</div>
|
||||
<div className="mt-1 text-xs uppercase tracking-[0.18em] text-slate-500">{collection.featured ? 'Featured • ' : ''}{collection.official ? 'Official' : '@' + (collection.owner?.username || 'creator')}</div>
|
||||
</div>
|
||||
<span className="rounded-full border border-white/10 bg-white/[0.04] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-200">{collection.cards_count} cards</span>
|
||||
</div>
|
||||
{collection.description ? <div className="mt-2 text-sm text-slate-400">{collection.description}</div> : null}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-6">
|
||||
<div className="rounded-[28px] border border-white/10 bg-white/[0.04] p-5 shadow-[0_20px_50px_rgba(2,6,23,0.18)]">
|
||||
<div className="mb-4 text-sm font-semibold uppercase tracking-[0.2em] text-slate-400">Collection editor</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<label className="text-sm text-slate-300">
|
||||
<span className="mb-2 block">Owner</span>
|
||||
<select value={form.user_id} onChange={(event) => setForm((current) => ({ ...current, user_id: Number(event.target.value) }))} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white">
|
||||
{admins.map((admin) => <option key={admin.id} value={admin.id}>{admin.name || admin.username}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<label className="text-sm text-slate-300">
|
||||
<span className="mb-2 block">Visibility</span>
|
||||
<select value={form.visibility} onChange={(event) => setForm((current) => ({ ...current, visibility: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white">
|
||||
<option value="public">public</option>
|
||||
<option value="private">private</option>
|
||||
</select>
|
||||
</label>
|
||||
<input value={form.name} onChange={(event) => setForm((current) => ({ ...current, name: event.target.value }))} placeholder="Collection name" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" />
|
||||
<input value={form.slug} onChange={(event) => setForm((current) => ({ ...current, slug: event.target.value }))} placeholder="Slug" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" />
|
||||
<textarea value={form.description} onChange={(event) => setForm((current) => ({ ...current, description: event.target.value }))} placeholder="Description" rows={4} className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white md:col-span-2" />
|
||||
</div>
|
||||
<div className="mt-5 flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200">
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<label className="flex items-center gap-2"><input type="checkbox" checked={Boolean(form.official)} onChange={(event) => setForm((current) => ({ ...current, official: event.target.checked }))} className="h-4 w-4" /> Official collection</label>
|
||||
<label className="flex items-center gap-2"><input type="checkbox" checked={Boolean(form.featured)} onChange={(event) => setForm((current) => ({ ...current, featured: event.target.checked }))} className="h-4 w-4" /> Featured collection</label>
|
||||
</div>
|
||||
{selected?.public_url ? <a href={selected.public_url} className="text-sky-100 transition hover:text-white" target="_blank" rel="noreferrer">Open public page</a> : null}
|
||||
</div>
|
||||
<button type="button" onClick={saveCollection} className="mt-5 w-full rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15">{selectedId ? 'Update collection' : 'Create collection'}</button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[28px] border border-white/10 bg-white/[0.04] p-5 shadow-[0_20px_50px_rgba(2,6,23,0.18)]">
|
||||
<div className="mb-4 text-sm font-semibold uppercase tracking-[0.2em] text-slate-400">Curate cards</div>
|
||||
{!selectedId ? (
|
||||
<div className="rounded-2xl border border-dashed border-white/12 bg-white/[0.03] px-4 py-8 text-center text-sm text-slate-400">Create or select a collection first.</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto]">
|
||||
<select value={cardId} onChange={(event) => setCardId(event.target.value)} className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white">
|
||||
<option value="">Select a card</option>
|
||||
{cards.map((card) => <option key={card.id} value={card.id}>{card.title}</option>)}
|
||||
</select>
|
||||
<input value={cardNote} onChange={(event) => setCardNote(event.target.value)} placeholder="Optional curator note" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" />
|
||||
<button type="button" onClick={attachCard} className="rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15">Add</button>
|
||||
</div>
|
||||
<div className="mt-5 space-y-3">
|
||||
{(selected?.items || []).map((item) => (
|
||||
<div key={item.id} className="flex items-start justify-between gap-4 rounded-[22px] border border-white/10 bg-white/[0.03] p-4">
|
||||
<div>
|
||||
<div className="text-base font-semibold text-white">{item.card?.title}</div>
|
||||
<div className="mt-1 text-xs uppercase tracking-[0.18em] text-slate-500">#{item.sort_order} {item.card?.creator?.username ? `• @${item.card.creator.username}` : ''}</div>
|
||||
{item.note ? <div className="mt-2 text-sm text-slate-400">{item.note}</div> : null}
|
||||
</div>
|
||||
<button type="button" onClick={() => detachCard(selectedId, item.card.id)} className="rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm font-semibold text-rose-100 transition hover:bg-rose-400/15">Remove</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user