Implement creator studio and upload updates
This commit is contained in:
127
resources/js/Pages/Studio/StudioInbox.jsx
Normal file
127
resources/js/Pages/Studio/StudioInbox.jsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import React, { useState } from 'react'
|
||||
import { router, usePage } from '@inertiajs/react'
|
||||
import StudioLayout from '../../Layouts/StudioLayout'
|
||||
|
||||
async function requestJson(url, method = 'POST') {
|
||||
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',
|
||||
},
|
||||
})
|
||||
const payload = await response.json().catch(() => ({}))
|
||||
if (!response.ok) throw new Error(payload?.message || 'Request failed')
|
||||
return payload
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) return 'Unknown'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return 'Unknown'
|
||||
return date.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })
|
||||
}
|
||||
|
||||
const priorityClasses = {
|
||||
high: 'border-rose-300/20 bg-rose-300/10 text-rose-100',
|
||||
medium: 'border-sky-300/20 bg-sky-300/10 text-sky-100',
|
||||
low: 'border-white/10 bg-white/[0.03] text-slate-300',
|
||||
}
|
||||
|
||||
export default function StudioInbox() {
|
||||
const { props } = usePage()
|
||||
const inbox = props.inbox || {}
|
||||
const filters = inbox.filters || {}
|
||||
const items = inbox.items || []
|
||||
const meta = inbox.meta || {}
|
||||
const summary = inbox.summary || {}
|
||||
const [marking, setMarking] = useState(false)
|
||||
|
||||
const updateFilters = (patch) => {
|
||||
const next = { ...filters, ...patch }
|
||||
if (patch.page == null) next.page = 1
|
||||
router.get(window.location.pathname, next, {
|
||||
preserveScroll: true,
|
||||
preserveState: true,
|
||||
replace: true,
|
||||
})
|
||||
}
|
||||
|
||||
const markAllRead = async () => {
|
||||
setMarking(true)
|
||||
try {
|
||||
await requestJson(props.endpoints.markAllRead)
|
||||
router.reload({ preserveScroll: true, preserveState: true })
|
||||
} catch (error) {
|
||||
window.alert(error?.message || 'Unable to mark inbox as read.')
|
||||
} finally {
|
||||
setMarking(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<StudioLayout title={props.title} subtitle={props.description} actions={<button type="button" onClick={markAllRead} disabled={marking} className="inline-flex items-center gap-2 rounded-full border border-white/10 px-4 py-2 text-sm text-slate-100 disabled:opacity-50"><i className="fa-solid fa-check-double" />{marking ? 'Updating...' : 'Mark all read'}</button>}>
|
||||
<div className="space-y-6">
|
||||
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="rounded-[24px] border border-white/10 bg-white/[0.03] p-5"><div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Unread</div><div className="mt-2 text-3xl font-semibold text-white">{Number(summary.unread_count || 0).toLocaleString()}</div></div>
|
||||
<div className="rounded-[24px] border border-white/10 bg-white/[0.03] p-5"><div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">High priority</div><div className="mt-2 text-3xl font-semibold text-white">{Number(summary.high_priority_count || 0).toLocaleString()}</div></div>
|
||||
<div className="rounded-[24px] border border-white/10 bg-white/[0.03] p-5"><div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Comments</div><div className="mt-2 text-3xl font-semibold text-white">{Number(summary.comment_count || 0).toLocaleString()}</div></div>
|
||||
<div className="rounded-[24px] border border-white/10 bg-white/[0.03] p-5"><div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Followers</div><div className="mt-2 text-3xl font-semibold text-white">{Number(summary.follower_count || 0).toLocaleString()}</div></div>
|
||||
</section>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[320px_minmax(0,1fr)]">
|
||||
<aside className="space-y-6">
|
||||
<section className="rounded-[28px] border border-white/10 bg-white/[0.03] p-5">
|
||||
<h2 className="text-lg font-semibold text-white">Filters</h2>
|
||||
<div className="mt-4 space-y-3">
|
||||
<label className="space-y-2 text-sm text-slate-300"><span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Search</span><input value={filters.q || ''} onChange={(event) => updateFilters({ q: event.target.value })} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white" placeholder="Actor, title, or module" /></label>
|
||||
<label className="space-y-2 text-sm text-slate-300"><span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Type</span><select value={filters.type || 'all'} onChange={(event) => updateFilters({ type: event.target.value })} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white">{(inbox.type_options || []).map((option) => <option key={option.value} value={option.value} className="bg-slate-900">{option.label}</option>)}</select></label>
|
||||
<label className="space-y-2 text-sm text-slate-300"><span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Module</span><select value={filters.module || 'all'} onChange={(event) => updateFilters({ module: event.target.value })} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white">{(inbox.module_options || []).map((option) => <option key={option.value} value={option.value} className="bg-slate-900">{option.label}</option>)}</select></label>
|
||||
<label className="space-y-2 text-sm text-slate-300"><span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Read state</span><select value={filters.read_state || 'all'} onChange={(event) => updateFilters({ read_state: event.target.value })} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white">{(inbox.read_state_options || []).map((option) => <option key={option.value} value={option.value} className="bg-slate-900">{option.label}</option>)}</select></label>
|
||||
<label className="space-y-2 text-sm text-slate-300"><span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Priority</span><select value={filters.priority || 'all'} onChange={(event) => updateFilters({ priority: event.target.value })} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white">{(inbox.priority_options || []).map((option) => <option key={option.value} value={option.value} className="bg-slate-900">{option.label}</option>)}</select></label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[28px] border border-white/10 bg-white/[0.03] p-5">
|
||||
<h2 className="text-lg font-semibold text-white">Attention now</h2>
|
||||
<div className="mt-4 space-y-3">{(inbox.panels?.attention_now || []).map((item) => <a key={item.id} href={item.url} className="block rounded-2xl border border-white/10 bg-black/20 p-4"><div className="text-sm font-semibold text-white">{item.title}</div><div className="mt-1 text-xs text-slate-500">{item.module_label}</div></a>)}</div>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<section className="space-y-4">
|
||||
{items.length > 0 ? items.map((item) => (
|
||||
<article key={item.id} className={`rounded-[28px] border p-5 ${item.is_new ? 'border-sky-300/20 bg-sky-300/10' : 'border-white/10 bg-white/[0.03]'}`}>
|
||||
<div className="flex gap-4">
|
||||
{item.actor?.avatar_url ? <img src={item.actor.avatar_url} alt={item.actor.name || 'Actor'} className="h-12 w-12 rounded-2xl object-cover" /> : <div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-black/20 text-slate-400"><i className="fa-solid fa-bell" /></div>}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">
|
||||
<span>{item.module_label}</span>
|
||||
<span className={`inline-flex items-center rounded-full border px-2 py-1 ${priorityClasses[item.priority] || priorityClasses.low}`}>{item.priority}</span>
|
||||
{item.is_new && <span className="rounded-full bg-sky-300/20 px-2 py-1 text-sky-100">Unread</span>}
|
||||
</div>
|
||||
<h2 className="mt-2 text-lg font-semibold text-white">{item.title}</h2>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-400">{item.body}</p>
|
||||
<div className="mt-4 flex flex-wrap items-center gap-3 text-sm text-slate-400">
|
||||
<span>{formatDate(item.created_at)}</span>
|
||||
{item.actor?.name && <span>{item.actor.name}</span>}
|
||||
<a href={item.url} className="inline-flex items-center gap-2 rounded-full border border-white/10 px-3 py-1.5 text-slate-200">Open</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
)) : <div className="rounded-[28px] border border-dashed border-white/15 px-6 py-16 text-center text-slate-400">No inbox items match this filter.</div>}
|
||||
|
||||
<div className="flex items-center justify-between rounded-[24px] border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-300">
|
||||
<button type="button" disabled={(meta.current_page || 1) <= 1} onClick={() => updateFilters({ page: Math.max(1, (meta.current_page || 1) - 1) })} className="rounded-full border border-white/10 px-4 py-2 disabled:opacity-40">Previous</button>
|
||||
<span className="text-xs uppercase tracking-[0.18em] text-slate-500">Page {meta.current_page || 1} of {meta.last_page || 1}</span>
|
||||
<button type="button" disabled={(meta.current_page || 1) >= (meta.last_page || 1)} onClick={() => updateFilters({ page: (meta.current_page || 1) + 1 })} className="rounded-full border border-white/10 px-4 py-2 disabled:opacity-40">Next</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</StudioLayout>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user