209 lines
9.0 KiB
JavaScript
209 lines
9.0 KiB
JavaScript
import React from 'react'
|
|
import { usePage } from '@inertiajs/react'
|
|
import StudioLayout from '../../Layouts/StudioLayout'
|
|
import StudioToolbar from '../../Components/Studio/StudioToolbar'
|
|
import StudioGridCard from '../../Components/Studio/StudioGridCard'
|
|
import StudioTable from '../../Components/Studio/StudioTable'
|
|
import BulkActionsBar from '../../Components/Studio/BulkActionsBar'
|
|
import BulkTagModal from '../../Components/Studio/BulkTagModal'
|
|
import BulkCategoryModal from '../../Components/Studio/BulkCategoryModal'
|
|
import ConfirmDangerModal from '../../Components/Studio/ConfirmDangerModal'
|
|
|
|
function getCsrfToken() {
|
|
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
|
}
|
|
|
|
export default function StudioDrafts() {
|
|
const { props } = usePage()
|
|
const { categories } = props
|
|
|
|
const [viewMode, setViewMode] = React.useState(() => localStorage.getItem('studio_view_mode') || 'grid')
|
|
const [artworks, setArtworks] = React.useState([])
|
|
const [meta, setMeta] = React.useState({ current_page: 1, last_page: 1, per_page: 24, total: 0 })
|
|
const [loading, setLoading] = React.useState(true)
|
|
const [search, setSearch] = React.useState('')
|
|
const [sort, setSort] = React.useState('created_at:desc')
|
|
const [selectedIds, setSelectedIds] = React.useState([])
|
|
const [deleteModal, setDeleteModal] = React.useState({ open: false, ids: [] })
|
|
const [tagModal, setTagModal] = React.useState({ open: false, mode: 'add' })
|
|
const [categoryModal, setCategoryModal] = React.useState({ open: false })
|
|
const searchTimer = React.useRef(null)
|
|
const perPage = viewMode === 'list' ? 50 : 24
|
|
|
|
function getCsrfToken() {
|
|
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
|
}
|
|
|
|
const fetchArtworks = React.useCallback(async (page = 1) => {
|
|
setLoading(true)
|
|
try {
|
|
const params = new URLSearchParams()
|
|
params.set('page', page)
|
|
params.set('per_page', perPage)
|
|
params.set('sort', sort)
|
|
params.set('status', 'draft')
|
|
if (search) params.set('q', search)
|
|
const res = await fetch(`/api/studio/artworks?${params.toString()}`, {
|
|
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': getCsrfToken() },
|
|
credentials: 'same-origin',
|
|
})
|
|
const data = await res.json()
|
|
setArtworks(data.data || [])
|
|
setMeta(data.meta || meta)
|
|
} catch (err) {
|
|
console.error('Failed to fetch:', err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [search, sort, perPage])
|
|
|
|
React.useEffect(() => {
|
|
clearTimeout(searchTimer.current)
|
|
searchTimer.current = setTimeout(() => fetchArtworks(1), 300)
|
|
return () => clearTimeout(searchTimer.current)
|
|
}, [fetchArtworks])
|
|
|
|
const handleViewModeChange = (mode) => {
|
|
setViewMode(mode)
|
|
localStorage.setItem('studio_view_mode', mode)
|
|
}
|
|
|
|
const toggleSelect = (id) => setSelectedIds((p) => p.includes(id) ? p.filter((i) => i !== id) : [...p, id])
|
|
const selectAll = () => {
|
|
const ids = artworks.map((a) => a.id)
|
|
setSelectedIds(ids.every((id) => selectedIds.includes(id)) ? [] : ids)
|
|
}
|
|
|
|
const handleAction = async (action, artwork) => {
|
|
if (action === 'edit') { window.location.href = `/studio/artworks/${artwork.id}/edit`; return }
|
|
if (action === 'delete') { setDeleteModal({ open: true, ids: [artwork.id] }); return }
|
|
try {
|
|
await fetch(`/api/studio/artworks/${artwork.id}/toggle`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': getCsrfToken() },
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify({ action }),
|
|
})
|
|
fetchArtworks(meta.current_page)
|
|
} catch (err) { console.error(err) }
|
|
}
|
|
|
|
const executeBulk = async (action) => {
|
|
if (action === 'delete') { setDeleteModal({ open: true, ids: [...selectedIds] }); return }
|
|
if (action === 'add_tags') { setTagModal({ open: true, mode: 'add' }); return }
|
|
if (action === 'remove_tags') { setTagModal({ open: true, mode: 'remove' }); return }
|
|
if (action === 'change_category') { setCategoryModal({ open: true }); return }
|
|
try {
|
|
await fetch('/api/studio/artworks/bulk', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': getCsrfToken() },
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify({ action, artwork_ids: selectedIds, params: {} }),
|
|
})
|
|
setSelectedIds([])
|
|
fetchArtworks(meta.current_page)
|
|
} catch (err) { console.error(err) }
|
|
}
|
|
|
|
const confirmBulkTags = async (tagIds) => {
|
|
const action = tagModal.mode === 'add' ? 'add_tags' : 'remove_tags'
|
|
setTagModal({ open: false, mode: 'add' })
|
|
try {
|
|
await fetch('/api/studio/artworks/bulk', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': getCsrfToken() },
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify({ action, artwork_ids: selectedIds, params: { tag_ids: tagIds } }),
|
|
})
|
|
setSelectedIds([])
|
|
fetchArtworks(meta.current_page)
|
|
} catch (err) { console.error(err) }
|
|
}
|
|
|
|
const confirmBulkCategory = async (categoryId) => {
|
|
setCategoryModal({ open: false })
|
|
try {
|
|
await fetch('/api/studio/artworks/bulk', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': getCsrfToken() },
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify({ action: 'change_category', artwork_ids: selectedIds, params: { category_id: categoryId } }),
|
|
})
|
|
setSelectedIds([])
|
|
fetchArtworks(meta.current_page)
|
|
} catch (err) { console.error(err) }
|
|
}
|
|
|
|
const confirmDelete = async () => {
|
|
try {
|
|
await fetch('/api/studio/artworks/bulk', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': getCsrfToken() },
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify({ action: 'delete', artwork_ids: deleteModal.ids, confirm: 'DELETE' }),
|
|
})
|
|
setDeleteModal({ open: false, ids: [] })
|
|
setSelectedIds((p) => p.filter((id) => !deleteModal.ids.includes(id)))
|
|
fetchArtworks(meta.current_page)
|
|
} catch (err) { console.error(err) }
|
|
}
|
|
|
|
return (
|
|
<StudioLayout title="Drafts">
|
|
<StudioToolbar
|
|
search={search}
|
|
onSearchChange={setSearch}
|
|
sort={sort}
|
|
onSortChange={setSort}
|
|
viewMode={viewMode}
|
|
onViewModeChange={handleViewModeChange}
|
|
onFilterToggle={() => {}}
|
|
selectedCount={selectedIds.length}
|
|
/>
|
|
|
|
{loading && (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="w-8 h-8 border-2 border-accent/30 border-t-accent rounded-full animate-spin" />
|
|
</div>
|
|
)}
|
|
|
|
{!loading && viewMode === 'grid' && (
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
|
{artworks.map((art) => (
|
|
<StudioGridCard key={art.id} artwork={art} selected={selectedIds.includes(art.id)} onSelect={toggleSelect} onAction={handleAction} />
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{!loading && viewMode === 'list' && (
|
|
<StudioTable artworks={artworks} selectedIds={selectedIds} onSelect={toggleSelect} onSelectAll={selectAll} onAction={handleAction} onSort={setSort} currentSort={sort} />
|
|
)}
|
|
|
|
{!loading && artworks.length === 0 && (
|
|
<div className="text-center py-16">
|
|
<i className="fa-solid fa-file-pen text-4xl text-slate-600 mb-4" />
|
|
<p className="text-slate-500 text-sm">No draft artworks</p>
|
|
</div>
|
|
)}
|
|
|
|
{meta.last_page > 1 && (
|
|
<div className="flex items-center justify-center gap-2 mt-6">
|
|
{Array.from({ length: meta.last_page }, (_, i) => i + 1)
|
|
.filter((p) => p === 1 || p === meta.last_page || Math.abs(p - meta.current_page) <= 2)
|
|
.map((page, idx, arr) => (
|
|
<React.Fragment key={page}>
|
|
{idx > 0 && arr[idx - 1] !== page - 1 && <span className="text-slate-600 text-sm">…</span>}
|
|
<button onClick={() => fetchArtworks(page)} className={`w-9 h-9 rounded-xl text-sm font-medium transition-all ${page === meta.current_page ? 'bg-accent text-white' : 'text-slate-400 hover:text-white hover:bg-white/5'}`}>{page}</button>
|
|
</React.Fragment>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<BulkActionsBar count={selectedIds.length} onExecute={executeBulk} onClearSelection={() => setSelectedIds([])} />
|
|
<ConfirmDangerModal open={deleteModal.open} onClose={() => setDeleteModal({ open: false, ids: [] })} onConfirm={confirmDelete} title="Permanently delete?" message={`Delete ${deleteModal.ids.length} artwork(s) permanently?`} />
|
|
<BulkTagModal open={tagModal.open} mode={tagModal.mode} onClose={() => setTagModal({ open: false, mode: 'add' })} onConfirm={confirmBulkTags} />
|
|
<BulkCategoryModal open={categoryModal.open} categories={categories} onClose={() => setCategoryModal({ open: false })} onConfirm={confirmBulkCategory} />
|
|
</StudioLayout>
|
|
)
|
|
}
|