Commit workspace changes

This commit is contained in:
2026-04-05 19:42:33 +02:00
parent 148a3bbe43
commit 08ad757bcb
312 changed files with 35149 additions and 399 deletions

View 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}`
}