Commit workspace changes
This commit is contained in:
96
resources/js/Pages/Studio/StudioGroupJoinRequests.jsx
Normal file
96
resources/js/Pages/Studio/StudioGroupJoinRequests.jsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React from 'react'
|
||||
import { router, usePage } from '@inertiajs/react'
|
||||
import StudioLayout from '../../Layouts/StudioLayout'
|
||||
|
||||
function HistoryList({ items }) {
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
return <div className="rounded-2xl border border-dashed border-white/10 bg-white/[0.02] p-4 text-sm text-slate-400">No recent history yet.</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3">
|
||||
<div className="text-sm font-semibold text-white">{item.summary || item.action_type}</div>
|
||||
<div className="mt-1 text-xs text-slate-400">{item.actor?.name || item.actor?.username || 'System'} • {item.created_at ? new Date(item.created_at).toLocaleString() : 'Recently'}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function StudioGroupJoinRequests() {
|
||||
const { props } = usePage()
|
||||
const listing = props.listing || {}
|
||||
const items = Array.isArray(listing.items) ? listing.items : []
|
||||
|
||||
const approve = (request) => {
|
||||
const role = window.prompt('Role to assign on approval? contributor, editor, or admin', request.desired_role || 'contributor') || request.desired_role || 'contributor'
|
||||
const notes = window.prompt('Optional approval note', '') || ''
|
||||
router.post(request.can_approve ? routeUrl(props.studioGroup?.urls?.studio_join_requests, request.id, 'approve') : '', { action: 'approve', role, review_notes: notes })
|
||||
}
|
||||
|
||||
const reject = (request) => {
|
||||
const notes = window.prompt('Optional rejection note', '') || ''
|
||||
router.post(request.can_reject ? routeUrl(props.studioGroup?.urls?.studio_join_requests, request.id, 'reject') : '', { action: 'reject', review_notes: notes })
|
||||
}
|
||||
|
||||
return (
|
||||
<StudioLayout title={props.title} subtitle={props.description}>
|
||||
<div className="grid gap-6 xl:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
|
||||
<section className="rounded-[28px] border border-white/10 bg-white/[0.03] p-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-white">Incoming requests</h2>
|
||||
<p className="mt-1 text-sm text-slate-400">Approve, reject, and assign roles from one queue.</p>
|
||||
</div>
|
||||
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold uppercase tracking-[0.16em] text-slate-300">{listing.filters?.bucket || 'pending'}</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
{items.length > 0 ? items.map((item) => (
|
||||
<article key={item.id} className="rounded-[24px] border border-white/10 bg-black/20 p-4">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{item.user?.avatar_url ? <img src={item.user.avatar_url} alt={item.user.name || item.user.username} className="h-12 w-12 rounded-2xl object-cover" /> : <div className="flex h-12 w-12 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.03] text-slate-400"><i className="fa-solid fa-user" /></div>}
|
||||
<div>
|
||||
<div className="font-semibold text-white">{item.user?.name || item.user?.username}</div>
|
||||
<div className="text-sm text-slate-400">Requested role: {item.desired_role_label || item.desired_role || 'Contributor'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold uppercase tracking-[0.16em] text-slate-300">{item.status}</span>
|
||||
</div>
|
||||
{item.message ? <p className="mt-4 text-sm leading-6 text-slate-300">{item.message}</p> : null}
|
||||
<div className="mt-3 flex flex-wrap gap-3 text-xs text-slate-400">
|
||||
{item.portfolio_url ? <a href={item.portfolio_url} className="text-sky-200 underline underline-offset-4">Portfolio</a> : null}
|
||||
{Array.isArray(item.skills) && item.skills.length > 0 ? <span>{item.skills.join(', ')}</span> : null}
|
||||
{item.created_at ? <span>{new Date(item.created_at).toLocaleString()}</span> : null}
|
||||
</div>
|
||||
{item.review_notes ? <p className="mt-3 rounded-2xl border border-white/10 bg-white/[0.03] px-3 py-2 text-sm text-slate-300">{item.review_notes}</p> : null}
|
||||
{item.can_approve || item.can_reject ? (
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{item.can_approve ? <button type="button" onClick={() => approve(item)} className="rounded-full border border-emerald-300/20 bg-emerald-400/10 px-4 py-2 text-sm font-semibold text-emerald-100">Approve</button> : null}
|
||||
{item.can_reject ? <button type="button" onClick={() => reject(item)} className="rounded-full border border-rose-300/20 bg-rose-400/10 px-4 py-2 text-sm font-semibold text-rose-100">Reject</button> : null}
|
||||
</div>
|
||||
) : null}
|
||||
</article>
|
||||
)) : <div className="rounded-[24px] border border-dashed border-white/10 bg-white/[0.02] p-5 text-sm text-slate-400">No join requests in this bucket.</div>}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[28px] border border-white/10 bg-white/[0.03] p-5">
|
||||
<h2 className="text-xl font-semibold text-white">Recent history</h2>
|
||||
<p className="mt-1 text-sm text-slate-400">Audit trail for moderation-sensitive group actions.</p>
|
||||
<div className="mt-4">
|
||||
<HistoryList items={props.recentHistory} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</StudioLayout>
|
||||
)
|
||||
}
|
||||
|
||||
function routeUrl(baseUrl, id, action) {
|
||||
if (!baseUrl) return ''
|
||||
return `${String(baseUrl).replace(/\/$/, '')}/${id}/${action}`
|
||||
}
|
||||
Reference in New Issue
Block a user