Implement academy analytics, billing, and web stories updates
This commit is contained in:
@@ -31,11 +31,38 @@ function statusTone(status) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildPaginationPages(current, last) {
|
||||
if (last <= 1) return [1]
|
||||
if (last <= 7) {
|
||||
return Array.from({ length: last }, (_, index) => index + 1)
|
||||
}
|
||||
|
||||
const pages = new Set([1, 2, current - 1, current, current + 1, last - 1, last])
|
||||
const sorted = [...pages]
|
||||
.filter((page) => page >= 1 && page <= last)
|
||||
.sort((left, right) => left - right)
|
||||
|
||||
const result = []
|
||||
for (let index = 0; index < sorted.length; index += 1) {
|
||||
if (index > 0 && sorted[index] - sorted[index - 1] > 1) {
|
||||
result.push('ellipsis')
|
||||
}
|
||||
result.push(sorted[index])
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export default function StudioNewsIndex() {
|
||||
const { props } = usePage()
|
||||
const items = Array.isArray(props.listing?.items) ? props.listing.items : []
|
||||
const filters = props.listing?.filters || {}
|
||||
const meta = props.listing?.meta || {}
|
||||
const currentPage = Number(meta.current_page || 1)
|
||||
const lastPage = Number(meta.last_page || 1)
|
||||
const from = Number(meta.from || 0)
|
||||
const to = Number(meta.to || 0)
|
||||
const paginationPages = buildPaginationPages(currentPage, lastPage)
|
||||
|
||||
const deleteItem = (item) => {
|
||||
if (!item?.delete_url) return
|
||||
@@ -46,11 +73,13 @@ export default function StudioNewsIndex() {
|
||||
})
|
||||
}
|
||||
|
||||
const updateFilter = (next) => {
|
||||
const updateFilter = (next, resetPage = true) => {
|
||||
const hasExplicitPage = Object.prototype.hasOwnProperty.call(next, 'page')
|
||||
|
||||
router.get('/studio/news', {
|
||||
...filters,
|
||||
...next,
|
||||
page: 1,
|
||||
page: hasExplicitPage ? next.page : (resetPage ? 1 : currentPage),
|
||||
}, {
|
||||
preserveState: true,
|
||||
preserveScroll: true,
|
||||
@@ -94,6 +123,8 @@ export default function StudioNewsIndex() {
|
||||
status: filters.status || '',
|
||||
type: filters.type || '',
|
||||
category_id: filters.category_id || '',
|
||||
order: filters.order || '',
|
||||
direction: filters.direction || '',
|
||||
})
|
||||
}
|
||||
}}
|
||||
@@ -103,7 +134,7 @@ export default function StudioNewsIndex() {
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Status</span>
|
||||
<NovaSelect
|
||||
value={filters.status || ''}
|
||||
onChange={(value) => updateFilter({ status: value, q: filters.q || '', type: filters.type || '', category_id: filters.category_id || '' })}
|
||||
onChange={(value) => updateFilter({ status: value, q: filters.q || '', type: filters.type || '', category_id: filters.category_id || '', order: filters.order || '', direction: filters.direction || '' })}
|
||||
placeholder="All statuses"
|
||||
options={(Array.isArray(props.statusOptions) ? props.statusOptions : []).map((option) => ({ value: option.value, label: option.label }))}
|
||||
searchable={false}
|
||||
@@ -113,7 +144,7 @@ export default function StudioNewsIndex() {
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Type</span>
|
||||
<NovaSelect
|
||||
value={filters.type || ''}
|
||||
onChange={(value) => updateFilter({ type: value, q: filters.q || '', status: filters.status || '', category_id: filters.category_id || '' })}
|
||||
onChange={(value) => updateFilter({ type: value, q: filters.q || '', status: filters.status || '', category_id: filters.category_id || '', order: filters.order || '', direction: filters.direction || '' })}
|
||||
placeholder="All types"
|
||||
options={(Array.isArray(props.typeOptions) ? props.typeOptions : []).map((option) => ({ value: option.value, label: option.label }))}
|
||||
searchable={false}
|
||||
@@ -123,12 +154,53 @@ export default function StudioNewsIndex() {
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Category</span>
|
||||
<NovaSelect
|
||||
value={filters.category_id || ''}
|
||||
onChange={(value) => updateFilter({ category_id: value, q: filters.q || '', status: filters.status || '', type: filters.type || '' })}
|
||||
onChange={(value) => updateFilter({ category_id: value, q: filters.q || '', status: filters.status || '', type: filters.type || '', order: filters.order || '', direction: filters.direction || '' })}
|
||||
placeholder="All categories"
|
||||
options={(Array.isArray(props.categoryOptions) ? props.categoryOptions : []).map((option) => ({ value: String(option.id), label: option.name }))}
|
||||
searchable={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2 text-sm text-slate-300">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Order</span>
|
||||
<NovaSelect
|
||||
value={filters.order || ''}
|
||||
onChange={(value) => updateFilter({ order: value, q: filters.q || '', status: filters.status || '', type: filters.type || '', category_id: filters.category_id || '', direction: filters.direction || '' })}
|
||||
placeholder="Order by"
|
||||
options={[
|
||||
{ value: 'date', label: 'Date' },
|
||||
{ value: 'title', label: 'Title' },
|
||||
{ value: 'views', label: 'Views' },
|
||||
]}
|
||||
searchable={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2 text-sm text-slate-300">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Direction</span>
|
||||
<NovaSelect
|
||||
value={filters.direction || ''}
|
||||
onChange={(value) => updateFilter({ direction: value, q: filters.q || '', status: filters.status || '', type: filters.type || '', category_id: filters.category_id || '', order: filters.order || '' })}
|
||||
placeholder="Asc / Desc"
|
||||
options={[
|
||||
{ value: 'desc', label: 'Desc' },
|
||||
{ value: 'asc', label: 'Asc' },
|
||||
]}
|
||||
searchable={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2 text-sm text-slate-300">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Per page</span>
|
||||
<NovaSelect
|
||||
value={String(filters.per_page || 15)}
|
||||
onChange={(value) => updateFilter({ per_page: value })}
|
||||
placeholder="Per page"
|
||||
options={[
|
||||
{ value: '15', label: '15 articles' },
|
||||
{ value: '30', label: '30 articles' },
|
||||
{ value: '50', label: '50 articles' },
|
||||
]}
|
||||
searchable={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm text-slate-400 lg:text-right">{Number(meta.total || 0).toLocaleString()} articles</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -161,6 +233,48 @@ export default function StudioNewsIndex() {
|
||||
</article>
|
||||
)) : <div className="rounded-[24px] border border-dashed border-white/10 bg-white/[0.02] p-6 text-sm text-slate-400">No News articles match the current filters.</div>}
|
||||
</section>
|
||||
|
||||
{lastPage > 1 ? (
|
||||
<div className="mt-6 flex flex-col gap-3 rounded-[24px] border border-white/10 bg-white/[0.03] px-4 py-4 text-sm text-slate-300 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="text-slate-400">
|
||||
Showing {from.toLocaleString()}-{to.toLocaleString()} of {Number(meta.total || 0).toLocaleString()} articles
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={currentPage <= 1}
|
||||
onClick={() => updateFilter({ page: Math.max(1, currentPage - 1) }, false)}
|
||||
className="rounded-full border border-white/10 px-4 py-2 font-semibold text-white transition hover:bg-white/[0.06] disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
{paginationPages.map((page, index) => (
|
||||
page === 'ellipsis' ? (
|
||||
<span key={`ellipsis-${index}`} className="px-2 text-slate-500">...</span>
|
||||
) : (
|
||||
<button
|
||||
key={page}
|
||||
type="button"
|
||||
onClick={() => updateFilter({ page }, false)}
|
||||
aria-current={page === currentPage ? 'page' : undefined}
|
||||
className={`min-w-10 rounded-full border px-3 py-2 text-sm font-semibold transition ${page === currentPage ? 'border-sky-300/20 bg-sky-400/10 text-sky-100' : 'border-white/10 bg-white/[0.03] text-white hover:bg-white/[0.06]'}`}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
))}
|
||||
<span className="ml-1 text-xs uppercase tracking-[0.18em] text-slate-500">Page {currentPage} of {lastPage}</span>
|
||||
<button
|
||||
type="button"
|
||||
disabled={currentPage >= lastPage}
|
||||
onClick={() => updateFilter({ page: currentPage + 1 }, false)}
|
||||
className="rounded-full border border-white/10 px-4 py-2 font-semibold text-white transition hover:bg-white/[0.06] disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</StudioLayout>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user