import React, { useState } from 'react'
import { router, usePage } from '@inertiajs/react'
import SeoHead from '../../components/seo/SeoHead'
import useWebShare from '../../hooks/useWebShare'
import NovaSelect from '../../components/ui/NovaSelect'
function normalizeText(value) {
return String(value || '').trim().toLowerCase()
}
const MEMBER_ROLE_COLORS = {
owner: { badge: 'border-amber-300/25 bg-amber-400/10 text-amber-100', icon: 'fa-crown', iconColor: 'text-amber-300' },
admin: { badge: 'border-sky-300/25 bg-sky-400/10 text-sky-100', icon: 'fa-shield-halved', iconColor: 'text-sky-300' },
editor: { badge: 'border-violet-300/25 bg-violet-400/10 text-violet-100', icon: 'fa-pen-nib', iconColor: 'text-violet-300' },
contributor: { badge: 'border-emerald-300/25 bg-emerald-400/10 text-emerald-100', icon: 'fa-star', iconColor: 'text-emerald-300' },
}
const POST_TYPE_ICONS = {
announcement: { icon: 'fa-bullhorn', bar: 'from-sky-400/80 to-sky-300/30', bg: 'bg-sky-400/10', border: 'border-sky-300/20', text: 'text-sky-200' },
update: { icon: 'fa-rotate', bar: 'from-emerald-400/80 to-emerald-300/30', bg: 'bg-emerald-400/10', border: 'border-emerald-300/20', text: 'text-emerald-200' },
event: { icon: 'fa-calendar-days', bar: 'from-violet-400/80 to-violet-300/30', bg: 'bg-violet-400/10', border: 'border-violet-300/20', text: 'text-violet-200' },
news: { icon: 'fa-newspaper', bar: 'from-amber-400/80 to-amber-300/30', bg: 'bg-amber-400/10', border: 'border-amber-300/20', text: 'text-amber-200' },
discussion: { icon: 'fa-comments', bar: 'from-rose-400/80 to-rose-300/30', bg: 'bg-rose-400/10', border: 'border-rose-300/20', text: 'text-rose-200' },
tutorial: { icon: 'fa-graduation-cap', bar: 'from-teal-400/80 to-teal-300/30', bg: 'bg-teal-400/10', border: 'border-teal-300/20', text: 'text-teal-200' },
}
function formatCompactNumber(value) {
return Number(value ?? 0).toLocaleString()
}
function formatDateLabel(value) {
if (!value) return null
const date = new Date(value)
if (Number.isNaN(date.getTime())) return null
return date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })
}
function websiteLabel(url) {
if (!url) return null
try {
const parsed = new URL(url.startsWith('http') ? url : `https://${url}`)
return parsed.hostname
} catch {
return String(url).replace(/^https?:\/\//, '')
}
}
const SECTION_TABS = [
{ id: 'overview', label: 'Overview', icon: 'fa-compass' },
{ id: 'artworks', label: 'Artworks', icon: 'fa-images' },
{ id: 'collections', label: 'Collections', icon: 'fa-layer-group' },
{ id: 'posts', label: 'Posts', icon: 'fa-newspaper' },
{ id: 'projects', label: 'Projects', icon: 'fa-diagram-project' },
{ id: 'releases', label: 'Releases', icon: 'fa-rocket' },
{ id: 'challenges', label: 'Challenges', icon: 'fa-trophy' },
{ id: 'events', label: 'Events', icon: 'fa-calendar-days' },
{ id: 'activity', label: 'Activity', icon: 'fa-bolt' },
{ id: 'members', label: 'Members', icon: 'fa-users' },
{ id: 'about', label: 'About', icon: 'fa-id-card' },
]
function sectionHref(baseUrl, tab) {
return tab === 'overview' ? baseUrl : `${baseUrl}/${tab}`
}
function GroupTabs({ baseUrl, activeSection }) {
return (
)
}
function GroupHero({
group,
recruitment,
trustSignals,
following,
followersCount,
currentJoinRequest,
shareLabel,
onToggleFollow,
onJoinRequest,
onWithdrawJoinRequest,
onShare,
onReport,
reportEndpoint,
}) {
const activeSignals = Array.isArray(trustSignals) ? trustSignals.slice(0, 3) : []
const joinDate = formatDateLabel(group.founded_at || group.created_at)
const heroStats = [
{ label: 'Followers', value: formatCompactNumber(followersCount) },
{ label: 'Members', value: formatCompactNumber(group.counts?.members) },
{ label: 'Artworks', value: formatCompactNumber(group.counts?.artworks) },
{ label: 'Collections', value: formatCompactNumber(group.counts?.collections) },
]
return (
Group profile
{group.is_verified ? (
Verified
) : null}
{recruitment?.is_recruiting ? (
Recruiting
) : null}
{group.avatar_url ? (

) : (
)}
{group.name}
@{group.slug}
{group.visibility ?
{group.visibility} : null}
{group.status ?
{group.status} : null}
{group.type ?
{group.type} : null}
{joinDate ? (
Since {joinDate}
) : null}
{group.website_url ? (
{websiteLabel(group.website_url)}
) : null}
{group.headline ?
{group.headline}
: null}
{group.bio ?
{group.bio}
: null}
{activeSignals.length > 0 ? (
{activeSignals.map((signal) => (
{signal.label}
))}
) : null}
{group.urls?.studio ? (
Open Studio
) : null}
{group.urls?.follow ? (
) : null}
{group.permissions?.can_request_join ? (
) : null}
{currentJoinRequest?.status === 'pending' ? (
) : null}
{reportEndpoint ? (
) : null}
{heroStats.map((fact) => (
{fact.label}
{fact.value}
))}
{group.owner?.username || group.owner?.name ? (
Owner {group.owner?.username || group.owner?.name}
) : null}
{recruitment?.headline ? (
{recruitment.headline}
) : null}
)
}
function ArtworkGrid({ artworks, emptyLabel = 'No artworks yet.' }) {
if (!Array.isArray(artworks) || artworks.length === 0) {
return (
)
}
return (
)
}
function CollectionGrid({ collections, emptyLabel = 'No collections yet.' }) {
if (!Array.isArray(collections) || collections.length === 0) {
return (
)
}
return (
)
}
function CompactCardGrid({ items, emptyLabel, badgeKey = 'status' }) {
if (!Array.isArray(items) || items.length === 0) {
return (
)
}
return (
)
}
function ReleaseGrid({ releases, emptyLabel = 'No public releases yet.' }) {
if (!Array.isArray(releases) || releases.length === 0) {
return (
)
}
return (
)
}
function AssetGrid({ assets, emptyLabel = 'No public resources yet.' }) {
if (!Array.isArray(assets) || assets.length === 0) {
return {emptyLabel}
}
return (
)
}
function ActivityFeed({ items, emptyLabel = 'No public activity yet.' }) {
if (!Array.isArray(items) || items.length === 0) {
return {emptyLabel}
}
return (
{items.map((item) => (
{item.headline}
{item.is_pinned ? Pinned : null}
{item.summary ?
{item.summary}
: null}
{item.actor?.name || item.actor?.username || 'System'} • {item.occurred_at ? new Date(item.occurred_at).toLocaleString() : 'Recently'}
{item.subject?.url ?
Open : null}
))}
)
}
function LeadershipPreview({ leadership }) {
if (!Array.isArray(leadership) || leadership.length === 0) {
return null
}
return (
Leadership
Owner and admins
)
}
function FocusCard({ eyebrow, item, badgeKey = 'status', ctaLabel }) {
if (!item?.title) {
return null
}
return (
{eyebrow}
{item.title}
{item[badgeKey] ? {item[badgeKey]} : null}
{item.summary || 'Open for more details.'}
{ctaLabel}
)
}
function TrustSignalPanel({ signals }) {
if (!Array.isArray(signals) || signals.length === 0) {
return null
}
const TONE_STYLES = {
sky: { badge: 'border-sky-300/20 bg-sky-300/10 text-sky-100', dot: 'bg-sky-400', bar: 'from-sky-400/70 to-transparent' },
emerald: { badge: 'border-emerald-300/20 bg-emerald-300/10 text-emerald-100', dot: 'bg-emerald-400', bar: 'from-emerald-400/70 to-transparent' },
amber: { badge: 'border-amber-300/20 bg-amber-300/10 text-amber-100', dot: 'bg-amber-400', bar: 'from-amber-400/70 to-transparent' },
violet: { badge: 'border-violet-300/20 bg-violet-300/10 text-violet-100', dot: 'bg-violet-400', bar: 'from-violet-400/70 to-transparent' },
}
return (
Trust signals
How this group shows up
{signals.map((signal) => {
const ts = TONE_STYLES[signal.tone] || { badge: 'border-white/10 bg-white/[0.04] text-white', dot: 'bg-slate-400' }
return (
{signal.label}
)
})}
{signals.map((signal) => {
const ts = TONE_STYLES[signal.tone] || { badge: 'border-white/10 bg-white/[0.02] text-white', bar: 'from-slate-400/40 to-transparent' }
return (
c.startsWith('text-')) : 'text-white'}`}>{signal.label}
{signal.reason}
)
})}
)
}
function BadgeShowcase({ badges }) {
if (!Array.isArray(badges) || badges.length === 0) {
return null
}
return (
Badges
Earned group signals
{badges.map((badge) => (
{badge.awarded_at ?
{new Date(badge.awarded_at).toLocaleDateString()}
: null}
{badge.reason}
))}
)
}
function ContributorHighlights({ contributors }) {
if (!Array.isArray(contributors) || contributors.length === 0) {
return null
}
return (
Contributors
Trusted collaborators
)
}
function csrfToken() {
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
}
export default function GroupShow() {
const { props } = usePage()
const group = props.group || {}
const section = props.section || 'overview'
const featuredArtworks = Array.isArray(props.featuredArtworks) ? props.featuredArtworks : []
const artworks = Array.isArray(props.artworks) ? props.artworks : []
const featuredCollections = Array.isArray(props.featuredCollections) ? props.featuredCollections : []
const collections = Array.isArray(props.collections) ? props.collections : []
const posts = Array.isArray(props.posts) ? props.posts : []
const projects = Array.isArray(props.projects) ? props.projects : []
const releases = Array.isArray(props.releases) ? props.releases : []
const challenges = Array.isArray(props.challenges) ? props.challenges : []
const events = Array.isArray(props.events) ? props.events : []
const assets = Array.isArray(props.assets) ? props.assets : []
const activity = Array.isArray(props.activity) ? props.activity : []
const recruitment = props.recruitment || null
const currentJoinRequest = group.current_join_request || null
const leadership = Array.isArray(props.leadership) ? props.leadership : []
const members = Array.isArray(props.members) ? props.members : []
const topContributors = Array.isArray(props.topContributors) ? props.topContributors : []
const trustSignals = Array.isArray(props.trustSignals) ? props.trustSignals : []
const badgeShowcase = Array.isArray(props.badgeShowcase) ? props.badgeShowcase : []
const [following, setFollowing] = useState(Boolean(group.viewer?.is_following))
const [followersCount, setFollowersCount] = useState(Number(group.counts?.followers || 0))
const [shareLabel, setShareLabel] = useState('Share')
const [artworkQuery, setArtworkQuery] = useState('')
const [artworkSort, setArtworkSort] = useState('latest')
const contentShellClassName = section === 'artworks'
? 'mx-auto max-w-7xl px-4 md:px-6'
: section === 'overview' || section === 'posts'
? 'mx-auto max-w-7xl px-4 md:px-6'
: 'mx-auto max-w-6xl px-4'
const filteredArtworks = artworks
.filter((artwork) => {
const q = normalizeText(artworkQuery)
if (!q) return true
return normalizeText(artwork.title).includes(q) || normalizeText(artwork.author).includes(q)
})
.sort((left, right) => {
if (artworkSort === 'oldest') {
return new Date(left.published_at || 0).getTime() - new Date(right.published_at || 0).getTime()
}
if (artworkSort === 'title') {
return String(left.title || '').localeCompare(String(right.title || ''))
}
return new Date(right.published_at || 0).getTime() - new Date(left.published_at || 0).getTime()
})
const groupedMembers = {
owner: members.filter((member) => member.role === 'owner'),
admins: members.filter((member) => member.role === 'admin'),
editors: members.filter((member) => member.role === 'editor'),
contributors: members.filter((member) => member.role !== 'owner' && member.role !== 'admin' && member.role !== 'editor'),
}
const { share } = useWebShare({
onFallback: async ({ url }) => {
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(url)
setShareLabel('Link copied')
window.setTimeout(() => setShareLabel('Share'), 2000)
return
}
window.prompt('Copy this link', url)
},
})
const submitReport = async () => {
if (!props.reportEndpoint) return
const reason = window.prompt('Reason for reporting this group?')
if (!reason) return
await fetch(props.reportEndpoint, {
method: 'POST',
credentials: 'same-origin',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': csrfToken(),
},
body: JSON.stringify({ target_type: 'group', target_id: group.id, reason }),
})
}
const toggleFollow = async () => {
const response = await fetch(following ? group.urls?.unfollow : group.urls?.follow, {
method: following ? 'DELETE' : 'POST',
credentials: 'same-origin',
headers: {
Accept: 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': csrfToken(),
},
})
const payload = await response.json().catch(() => ({}))
if (response.ok) {
setFollowing(Boolean(payload?.following))
setFollowersCount(Number(payload?.followers_count || 0))
}
}
const handleShare = async () => {
const url = group.urls?.public || (typeof window !== 'undefined' ? window.location.href : '')
await share({
title: `${group.name} on Skinbase`,
text: group.headline || group.bio || 'Check out this Skinbase group.',
url,
})
}
const submitJoinRequest = async () => {
const message = window.prompt('Why do you want to join this group?') || ''
const desiredRole = window.prompt('Desired role: contributor, editor, or admin', 'contributor') || 'contributor'
router.post(group.urls?.join_request_store, { message, desired_role: desiredRole })
}
const withdrawJoinRequest = async () => {
if (!currentJoinRequest?.id || !group.urls?.join_request_withdraw_pattern) return
router.delete(group.urls.join_request_withdraw_pattern.replace('__JOIN_REQUEST__', String(currentJoinRequest.id)))
}
return (
{section === 'overview' ? (
0 ? featuredCollections : collections.slice(0, 2)} emptyLabel="No featured collections yet." />
{group.pinned_post ? (
{group.pinned_post.title}
{group.pinned_post.excerpt || 'Read the latest pinned update from this group.'}
Read post
) : null}
{recruitment?.is_recruiting ? (
Recruiting
{recruitment.headline || `${group.name} is looking for collaborators`}
{recruitment.description || 'This group is currently open to new contributors.'}
{Array.isArray(recruitment.roles) && recruitment.roles.length > 0 ? {recruitment.roles.map((role) => {role})}
: null}
) : null}
About {group.name}
{group.bio || 'No long-form description yet.'}
{group.founded_at ?
Founded {new Date(group.founded_at).toLocaleDateString()} : null}
{group.type ?
{group.type} : null}
{group.website_url ?
Website : null}
) : null}
{section === 'artworks' ? (
Artworks
Filter the group archive by title or contributor credit label, then change the sort order.
) : null}
{section === 'collections' ? (
) : null}
{section === 'posts' ? (
{posts.length > 0 ? (
) : (
No posts published yet.
Check back later for group updates and announcements.
)}
) : null}
{section === 'projects' ? (
Projects
Structured releases, collaboration hubs, and production pages published by this group.
) : null}
{section === 'releases' ? (
Releases
Published drops, milestone pipelines, and linked showcases from this group.
) : null}
{section === 'challenges' ? (
Challenges
Current and past prompts, internal sprints, and public-facing challenge runs.
) : null}
{section === 'events' ? (
Events
Launches, milestones, streams, and other moments on the group timeline.
) : null}
{section === 'activity' ? (
Activity
Public milestones from posts, releases, events, member changes, and challenge highlights.
) : null}
{section === 'members' ? (
{[
['Owner', 'owner', groupedMembers.owner],
['Admins', 'admin', groupedMembers.admins],
['Editors', 'editor', groupedMembers.editors],
['Contributors', 'contributor', groupedMembers.contributors],
].map(([label, roleKey, bucket]) => {
if (bucket.length === 0) return null
const roleStyle = MEMBER_ROLE_COLORS[roleKey] || MEMBER_ROLE_COLORS.contributor
return (
)
})}
) : null}
{section === 'about' ? (
About
{group.bio || 'No long-form description yet.'}
{group.website_url ?
{group.website_url}
: null}
{Array.isArray(group.links) && group.links.length > 0 ? (
) : null}
{group.founded_at ? Founded {new Date(group.founded_at).toLocaleDateString()} : null}
{group.type ? {group.type} : null}
) : null}
)
}