1328 lines
78 KiB
JavaScript
1328 lines
78 KiB
JavaScript
import React, { useState } from 'react'
|
|
import { router, usePage } from '@inertiajs/react'
|
|
import SeoHead from '../../components/seo/SeoHead'
|
|
import useWebShare from '../../hooks/useWebShare'
|
|
|
|
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 (
|
|
<div className="sticky top-0 z-30 border-b border-white/10 bg-[#08111f]/80 backdrop-blur-2xl">
|
|
<nav className="overflow-x-auto scrollbar-hide" aria-label="Group sections">
|
|
<div className="mx-auto flex w-max min-w-full gap-2 px-3 py-3 justify-center xl:items-stretch">
|
|
{SECTION_TABS.map((tab) => {
|
|
const isActive = activeSection === tab.id
|
|
|
|
return (
|
|
<a
|
|
key={tab.id}
|
|
href={sectionHref(baseUrl, tab.id)}
|
|
className={`group relative flex items-center gap-2.5 rounded-2xl border px-3.5 py-3 text-sm font-medium whitespace-nowrap outline-none transition-all duration-150 ${isActive
|
|
? 'border-sky-300/25 bg-gradient-to-br from-sky-400/18 via-white/[0.06] to-cyan-400/10 text-white shadow-[0_16px_32px_rgba(14,165,233,0.12)]'
|
|
: 'border-white/8 bg-white/[0.03] text-slate-400 hover:border-white/15 hover:bg-white/[0.05] hover:text-slate-100'
|
|
}`}
|
|
>
|
|
<span className={`inline-flex h-9 w-9 items-center justify-center rounded-xl border text-sm ${isActive ? 'border-sky-300/20 bg-sky-400/10 text-sky-200' : 'border-white/10 bg-white/[0.04] text-slate-500 group-hover:text-slate-300'}`}>
|
|
<i className={`fa-solid ${tab.icon} fa-fw`} />
|
|
</span>
|
|
{tab.label}
|
|
{isActive ? <span className="absolute inset-x-4 bottom-0 h-0.5 rounded-full bg-sky-300 shadow-[0_0_10px_rgba(125,211,252,0.8)]" aria-hidden="true" /> : null}
|
|
</a>
|
|
)
|
|
})}
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
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 (
|
|
<div className="relative mx-auto max-w-7xl px-4 pt-4 md:pt-6">
|
|
<div
|
|
aria-hidden="true"
|
|
className="pointer-events-none absolute inset-x-10 top-8 -z-10 h-44 rounded-full blur-3xl"
|
|
style={{
|
|
background: 'linear-gradient(90deg, rgba(56,189,248,0.18), rgba(16,185,129,0.14), rgba(59,130,246,0.12))',
|
|
}}
|
|
/>
|
|
|
|
<div className="relative overflow-hidden rounded-[32px] border border-white/10 bg-[#09111f]/80 shadow-[0_24px_80px_rgba(2,6,23,0.55)]">
|
|
<div
|
|
className="w-full h-[208px] md:h-[248px] xl:h-[288px]"
|
|
style={{
|
|
background: group.banner_url
|
|
? `url('${group.banner_url}') center center / cover no-repeat`
|
|
: 'linear-gradient(140deg, #07101d 0%, #0b1726 42%, #07111e 100%)',
|
|
position: 'relative',
|
|
}}
|
|
>
|
|
<div className="absolute left-4 top-4 z-20 flex flex-wrap items-center gap-2 md:left-6 md:top-6">
|
|
<span className="inline-flex items-center gap-2 rounded-full border border-white/15 bg-black/30 px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-200 backdrop-blur-md">
|
|
<span className="h-2 w-2 rounded-full bg-sky-400 shadow-[0_0_12px_rgba(56,189,248,0.9)]" />
|
|
Group profile
|
|
</span>
|
|
{group.is_verified ? (
|
|
<span className="inline-flex items-center gap-2 rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-100 backdrop-blur-md">
|
|
<i className="fa-solid fa-badge-check text-[10px]" />
|
|
Verified
|
|
</span>
|
|
) : null}
|
|
{recruitment?.is_recruiting ? (
|
|
<span className="inline-flex items-center gap-2 rounded-full border border-emerald-300/20 bg-emerald-300/10 px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.18em] text-emerald-100 backdrop-blur-md">
|
|
<i className="fa-solid fa-user-plus text-[10px]" />
|
|
Recruiting
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
|
|
<div
|
|
className="absolute inset-0"
|
|
style={{
|
|
background: group.banner_url
|
|
? 'linear-gradient(180deg, rgba(2,6,23,0.16) 0%, rgba(2,6,23,0.28) 38%, rgba(2,6,23,0.9) 100%)'
|
|
: 'radial-gradient(ellipse at 16% 40%, rgba(77,163,255,.18) 0%, transparent 60%), radial-gradient(ellipse at 84% 22%, rgba(16,185,129,.14) 0%, transparent 54%)',
|
|
}}
|
|
/>
|
|
<div className="absolute inset-0 opacity-[0.06] pointer-events-none" style={{ backgroundImage: 'url(/gfx/noise.png)', backgroundSize: '32px' }} />
|
|
</div>
|
|
|
|
<div className="relative px-4 pb-6 md:px-7 md:pb-7">
|
|
<div className="relative -mt-16 flex flex-col gap-5 md:-mt-20 md:flex-row md:items-start md:gap-6">
|
|
<div className="mx-auto z-10 shrink-0 md:mx-0">
|
|
<div className="flex h-[112px] w-[112px] items-center justify-center overflow-hidden rounded-[28px] border border-white/15 bg-[#0b1320] shadow-[0_0_0_8px_rgba(9,17,31,0.92),0_22px_44px_rgba(2,6,23,0.5)] md:h-[132px] md:w-[132px]">
|
|
{group.avatar_url ? (
|
|
<img src={group.avatar_url} alt={group.name} className="h-full w-full object-cover" />
|
|
) : (
|
|
<i className="fa-solid fa-people-group text-4xl text-slate-300" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="min-w-0 flex-1 text-center md:text-left">
|
|
<div className="grid gap-5 xl:grid-cols-[minmax(0,1fr)_430px] xl:items-start">
|
|
<div className="min-w-0">
|
|
<div className="flex flex-wrap items-center justify-center gap-2 md:justify-start">
|
|
<span className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-300">
|
|
<i className="fa-solid fa-stars text-[10px] text-sky-300" />
|
|
Publishing collective
|
|
</span>
|
|
{group.owner?.username || group.owner?.name ? (
|
|
<a href={group.owner?.profile_url || '#'} className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-300 transition hover:bg-white/[0.08] hover:text-white">
|
|
<i className="fa-solid fa-user-gear text-[10px] text-slate-400" />
|
|
Led by {group.owner?.username || group.owner?.name}
|
|
</a>
|
|
) : null}
|
|
</div>
|
|
|
|
<h1 className="mt-3 text-[30px] font-semibold leading-tight tracking-[-0.03em] text-white md:text-[42px]">
|
|
{group.name}
|
|
</h1>
|
|
<p className="mt-1 font-mono text-sm text-slate-400 md:text-[15px]">@{group.slug}</p>
|
|
|
|
<div className="mt-4 flex flex-wrap items-center justify-center gap-2 md:justify-start">
|
|
{group.visibility ? <span className="inline-flex items-center gap-1.5 rounded-full border border-white/10 bg-white/5 px-3 py-1.5 text-xs text-slate-300">{group.visibility}</span> : null}
|
|
{group.status ? <span className="inline-flex items-center gap-1.5 rounded-full border border-white/10 bg-white/5 px-3 py-1.5 text-xs text-slate-300">{group.status}</span> : null}
|
|
{group.type ? <span className="inline-flex items-center gap-1.5 rounded-full border border-white/10 bg-white/5 px-3 py-1.5 text-xs text-slate-300">{group.type}</span> : null}
|
|
{joinDate ? (
|
|
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/10 bg-white/5 px-3 py-1.5 text-xs text-slate-300">
|
|
<i className="fa-solid fa-calendar-days fa-fw text-slate-500" />
|
|
Since {joinDate}
|
|
</span>
|
|
) : null}
|
|
{group.website_url ? (
|
|
<a
|
|
href={group.website_url.startsWith('http') ? group.website_url : `https://${group.website_url}`}
|
|
target="_blank"
|
|
rel="nofollow noopener noreferrer"
|
|
className="inline-flex items-center gap-1.5 rounded-full border border-sky-400/20 bg-sky-400/10 px-3 py-1.5 text-xs text-sky-200 transition-colors hover:border-sky-300/35 hover:bg-sky-400/15"
|
|
>
|
|
<i className="fa-solid fa-link fa-fw" />
|
|
{websiteLabel(group.website_url)}
|
|
</a>
|
|
) : null}
|
|
</div>
|
|
|
|
{group.headline ? <p className="mx-auto mt-4 max-w-2xl text-sm leading-relaxed text-slate-300/90 md:mx-0 md:text-[15px]">{group.headline}</p> : null}
|
|
{group.bio ? <p className="mx-auto mt-3 max-w-3xl text-sm leading-relaxed text-slate-400 md:mx-0 line-clamp-3">{group.bio}</p> : null}
|
|
|
|
{activeSignals.length > 0 ? (
|
|
<div className="mt-4 flex flex-wrap items-center justify-center gap-2 md:justify-start">
|
|
{activeSignals.map((signal) => (
|
|
<span key={signal.key} className="inline-flex items-center gap-1.5 rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5 text-[11px] font-medium text-slate-300">
|
|
<span className="h-1.5 w-1.5 rounded-full bg-cyan-300" />
|
|
{signal.label}
|
|
</span>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="space-y-3 xl:pt-1">
|
|
<div className="flex flex-wrap items-center justify-center gap-2 xl:flex-nowrap xl:justify-end">
|
|
{group.urls?.studio ? (
|
|
<a
|
|
href={group.urls.studio}
|
|
className="inline-flex shrink-0 items-center gap-2 whitespace-nowrap rounded-2xl bg-gradient-to-r from-sky-500 to-cyan-400 px-4 py-2.5 text-sm font-semibold text-slate-950 shadow-[0_18px_36px_rgba(14,165,233,0.28)] transition-transform hover:-translate-y-0.5"
|
|
>
|
|
<i className="fa-solid fa-wand-magic-sparkles fa-fw" />
|
|
Open Studio
|
|
</a>
|
|
) : null}
|
|
{group.urls?.follow ? (
|
|
<button
|
|
type="button"
|
|
onClick={onToggleFollow}
|
|
className={`inline-flex shrink-0 items-center gap-2 whitespace-nowrap rounded-2xl border px-4 py-2.5 text-sm font-medium transition-all ${following ? 'border-emerald-400/40 bg-emerald-500/12 text-emerald-300 hover:bg-emerald-500/18' : 'border-sky-400/40 bg-sky-500/12 text-sky-200 hover:bg-sky-500/20'}`}
|
|
>
|
|
<i className={`fa-solid ${following ? 'fa-circle-check' : 'fa-user-plus'} fa-fw`} />
|
|
{following ? 'Following' : 'Follow group'}
|
|
</button>
|
|
) : null}
|
|
{group.permissions?.can_request_join ? (
|
|
<button
|
|
type="button"
|
|
onClick={onJoinRequest}
|
|
className="inline-flex shrink-0 items-center gap-2 whitespace-nowrap rounded-2xl border border-emerald-300/25 bg-emerald-300/10 px-4 py-2.5 text-sm font-medium text-emerald-100 transition hover:bg-emerald-300/15"
|
|
>
|
|
<i className="fa-solid fa-door-open fa-fw" />
|
|
Request to join
|
|
</button>
|
|
) : null}
|
|
{currentJoinRequest?.status === 'pending' ? (
|
|
<button
|
|
type="button"
|
|
onClick={onWithdrawJoinRequest}
|
|
className="inline-flex shrink-0 items-center gap-2 whitespace-nowrap rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-2.5 text-sm font-medium text-slate-300 transition-all hover:bg-white/[0.08] hover:text-white"
|
|
>
|
|
<i className="fa-solid fa-xmark fa-fw" />
|
|
Withdraw request
|
|
</button>
|
|
) : null}
|
|
<button
|
|
type="button"
|
|
onClick={onShare}
|
|
className="inline-flex shrink-0 items-center gap-2 whitespace-nowrap rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-2.5 text-sm font-medium text-slate-300 transition-all hover:bg-white/[0.08] hover:text-white"
|
|
>
|
|
<i className="fa-solid fa-share-nodes fa-fw" />
|
|
{shareLabel}
|
|
</button>
|
|
{reportEndpoint ? (
|
|
<button
|
|
type="button"
|
|
onClick={onReport}
|
|
className="inline-flex shrink-0 items-center gap-2 whitespace-nowrap rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-2.5 text-sm font-medium text-slate-300 transition-all hover:bg-white/[0.08] hover:text-white"
|
|
>
|
|
<i className="fa-solid fa-flag fa-fw" />
|
|
Report
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="rounded-[24px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.72),rgba(9,17,31,0.92))] p-3 text-left shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]">
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{heroStats.map((fact) => (
|
|
<div key={fact.label} className="rounded-2xl border border-white/10 bg-white/[0.04] px-3 py-2.5">
|
|
<div className="text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500">{fact.label}</div>
|
|
<div className="mt-1 text-sm font-semibold tracking-tight text-white md:text-[15px]">{fact.value}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="mt-2.5 flex flex-wrap items-center gap-2">
|
|
{group.owner?.username || group.owner?.name ? (
|
|
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5 text-[11px] font-medium text-slate-300">
|
|
<i className="fa-solid fa-crown text-[10px] text-amber-300" />
|
|
Owner {group.owner?.username || group.owner?.name}
|
|
</span>
|
|
) : null}
|
|
{recruitment?.headline ? (
|
|
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5 text-[11px] font-medium text-slate-300">
|
|
<i className="fa-solid fa-bullhorn text-[10px] text-sky-300" />
|
|
{recruitment.headline}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ArtworkGrid({ artworks, emptyLabel = 'No artworks yet.' }) {
|
|
if (!Array.isArray(artworks) || artworks.length === 0) {
|
|
return (
|
|
<div className="mt-5 flex flex-col items-center gap-3 rounded-[24px] border border-white/8 bg-white/[0.02] py-10 text-center">
|
|
<span className="inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.04] text-slate-500">
|
|
<i className="fa-solid fa-images text-2xl" />
|
|
</span>
|
|
<p className="text-sm text-slate-400">{emptyLabel}</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="mt-5 grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
|
{artworks.map((artwork) => (
|
|
<a key={artwork.id} href={artwork.url} className="group relative overflow-hidden rounded-[24px] border border-white/10 bg-black/20 transition hover:border-sky-300/30 hover:shadow-[0_8px_32px_rgba(56,189,248,0.08)]">
|
|
{artwork.thumb ? (
|
|
<div className="relative overflow-hidden aspect-[4/3]">
|
|
<img src={artwork.thumb} alt={artwork.title} className="h-full w-full object-cover transition duration-300 group-hover:scale-[1.03]" />
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 transition group-hover:opacity-100" />
|
|
</div>
|
|
) : (
|
|
<div className="flex aspect-[4/3] items-center justify-center bg-white/[0.03] text-slate-500">
|
|
<i className="fa-solid fa-image text-3xl" />
|
|
</div>
|
|
)}
|
|
<div className="p-4">
|
|
<h3 className="text-base font-semibold text-white">{artwork.title}</h3>
|
|
{artwork.author ? (
|
|
<span className="mt-2 inline-flex items-center gap-1.5 rounded-full border border-white/8 bg-white/[0.04] px-2.5 py-1 text-[11px] font-medium text-slate-300">
|
|
<i className="fa-solid fa-user-pen fa-fw text-slate-500" />{artwork.author}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
</a>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function CollectionGrid({ collections, emptyLabel = 'No collections yet.' }) {
|
|
if (!Array.isArray(collections) || collections.length === 0) {
|
|
return (
|
|
<div className="mt-5 flex flex-col items-center gap-3 rounded-[24px] border border-white/8 bg-white/[0.02] py-10 text-center">
|
|
<span className="inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.04] text-slate-500">
|
|
<i className="fa-solid fa-layer-group text-2xl" />
|
|
</span>
|
|
<p className="text-sm text-slate-400">{emptyLabel}</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="mt-5 grid gap-4 md:grid-cols-2">
|
|
{collections.map((collection) => (
|
|
<a key={collection.id} href={collection.url} className="group relative overflow-hidden rounded-[24px] border border-white/10 bg-black/20 p-5 transition hover:border-sky-300/25 hover:shadow-[0_6px_24px_rgba(56,189,248,0.07)]">
|
|
<div className="absolute inset-x-0 top-0 h-[3px] rounded-t-[24px] bg-gradient-to-r from-sky-400/70 via-cyan-300/50 to-transparent" />
|
|
<div className="flex items-start gap-3">
|
|
<span className="mt-0.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-[14px] border border-sky-300/20 bg-sky-400/10 text-sky-200">
|
|
<i className="fa-solid fa-layer-group fa-fw text-sm" />
|
|
</span>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<h3 className="text-base font-semibold text-white">{collection.title}</h3>
|
|
{collection.is_featured ? (
|
|
<span className="shrink-0 rounded-full border border-amber-300/20 bg-amber-300/10 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-amber-100">Featured</span>
|
|
) : null}
|
|
</div>
|
|
<p className="mt-1.5 text-sm leading-6 text-slate-300">{collection.summary || collection.description_excerpt || 'Open to explore this collection.'}</p>
|
|
</div>
|
|
</div>
|
|
<div className="mt-2 flex justify-end">
|
|
<span className="text-xs font-semibold text-sky-300 opacity-0 transition group-hover:opacity-100">Browse <i className="fa-solid fa-arrow-right ml-0.5" /></span>
|
|
</div>
|
|
</a>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function CompactCardGrid({ items, emptyLabel, badgeKey = 'status' }) {
|
|
if (!Array.isArray(items) || items.length === 0) {
|
|
return (
|
|
<div className="mt-5 flex flex-col items-center gap-3 rounded-[24px] border border-white/8 bg-white/[0.02] py-10 text-center">
|
|
<span className="inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.04] text-slate-500">
|
|
<i className="fa-solid fa-folder-open text-2xl" />
|
|
</span>
|
|
<p className="text-sm text-slate-400">{emptyLabel}</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="mt-5 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
{items.map((item) => (
|
|
<a key={item.id} href={item.url} className="group relative overflow-hidden rounded-[24px] border border-white/10 bg-black/20 p-5 transition hover:border-white/20 hover:shadow-[0_6px_24px_rgba(2,6,23,0.4)]">
|
|
<div className="absolute inset-x-0 top-0 h-[3px] rounded-t-[24px] bg-gradient-to-r from-violet-400/60 via-sky-300/40 to-transparent" />
|
|
<div className="flex items-center justify-between gap-3">
|
|
<h3 className="text-base font-semibold text-white">{item.title}</h3>
|
|
{item[badgeKey] ? (
|
|
<span className="shrink-0 rounded-full border border-white/10 bg-white/[0.06] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-300">{item[badgeKey]}</span>
|
|
) : null}
|
|
</div>
|
|
<p className="mt-2 text-sm leading-6 text-slate-300">{item.summary || 'Open for more details.'}</p>
|
|
<div className="mt-3 flex justify-end">
|
|
<span className="text-xs font-semibold text-sky-300 opacity-0 transition group-hover:opacity-100">Open <i className="fa-solid fa-arrow-right ml-0.5" /></span>
|
|
</div>
|
|
</a>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ReleaseGrid({ releases, emptyLabel = 'No public releases yet.' }) {
|
|
if (!Array.isArray(releases) || releases.length === 0) {
|
|
return (
|
|
<div className="mt-5 flex flex-col items-center gap-3 rounded-[24px] border border-white/8 bg-white/[0.02] py-10 text-center">
|
|
<span className="inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.04] text-slate-500">
|
|
<i className="fa-solid fa-rocket text-2xl" />
|
|
</span>
|
|
<p className="text-sm text-slate-400">{emptyLabel}</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="mt-5 grid gap-5 md:grid-cols-2 xl:grid-cols-3">
|
|
{releases.map((release) => (
|
|
<a key={release.id} href={release.url} className="group relative overflow-hidden rounded-[24px] border border-white/10 bg-black/20 transition hover:border-sky-300/25 hover:shadow-[0_8px_32px_rgba(56,189,248,0.08)]">
|
|
{release.cover_url ? (
|
|
<div className="relative overflow-hidden aspect-[4/3]">
|
|
<img src={release.cover_url} alt={release.title} className="h-full w-full object-cover transition duration-300 group-hover:scale-[1.03]" />
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
|
|
<div className="absolute bottom-3 left-3 flex flex-wrap gap-1.5">
|
|
{release.status ? <span className="rounded-full border border-white/15 bg-black/50 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-white backdrop-blur-sm">{release.status}</span> : null}
|
|
{release.current_stage ? <span className="rounded-full border border-sky-300/25 bg-sky-400/20 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-sky-100 backdrop-blur-sm">{release.current_stage}</span> : null}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="relative flex aspect-[4/3] items-center justify-center bg-gradient-to-br from-sky-900/30 to-slate-900/40">
|
|
<i className="fa-solid fa-rocket text-3xl text-slate-400" />
|
|
<div className="absolute bottom-3 left-3 flex flex-wrap gap-1.5">
|
|
{release.status ? <span className="rounded-full border border-white/15 bg-black/50 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-white backdrop-blur-sm">{release.status}</span> : null}
|
|
{release.current_stage ? <span className="rounded-full border border-sky-300/25 bg-sky-400/20 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-sky-100 backdrop-blur-sm">{release.current_stage}</span> : null}
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className="p-5">
|
|
<h3 className="text-base font-semibold text-white">{release.title}</h3>
|
|
<p className="mt-1.5 text-sm leading-6 text-slate-300">{release.summary || 'Release overview and linked artworks.'}</p>
|
|
<div className="mt-3 flex items-center gap-4 text-xs text-slate-500">
|
|
<span><i className="fa-solid fa-images mr-1.5" />{release.counts?.artworks || 0}</span>
|
|
<span><i className="fa-solid fa-users mr-1.5" />{release.counts?.contributors || 0}</span>
|
|
<span><i className="fa-solid fa-flag-checkered mr-1.5" />{release.counts?.milestones || 0}</span>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function AssetGrid({ assets, emptyLabel = 'No public resources yet.' }) {
|
|
if (!Array.isArray(assets) || assets.length === 0) {
|
|
return <p className="mt-5 text-sm text-slate-400">{emptyLabel}</p>
|
|
}
|
|
|
|
return (
|
|
<div className="mt-5 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
{assets.map((asset) => (
|
|
<a key={asset.id} href={asset.download_url} className="rounded-[24px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20">
|
|
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">{asset.category}</div>
|
|
<h3 className="mt-2 text-base font-semibold text-white">{asset.title}</h3>
|
|
<p className="mt-2 text-sm leading-6 text-slate-300">{asset.description || 'Download this shared group asset.'}</p>
|
|
</a>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ActivityFeed({ items, emptyLabel = 'No public activity yet.' }) {
|
|
if (!Array.isArray(items) || items.length === 0) {
|
|
return <p className="mt-5 text-sm text-slate-400">{emptyLabel}</p>
|
|
}
|
|
|
|
return (
|
|
<div className="mt-5 space-y-3">
|
|
{items.map((item) => (
|
|
<div key={item.id} className="rounded-[24px] border border-white/10 bg-black/20 p-4">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<h3 className="text-base font-semibold text-white">{item.headline}</h3>
|
|
{item.is_pinned ? <span className="rounded-full border border-amber-300/20 bg-amber-300/10 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-amber-100">Pinned</span> : null}
|
|
</div>
|
|
{item.summary ? <p className="mt-2 text-sm leading-6 text-slate-300">{item.summary}</p> : null}
|
|
<div className="mt-2 text-xs text-slate-500">{item.actor?.name || item.actor?.username || 'System'} • {item.occurred_at ? new Date(item.occurred_at).toLocaleString() : 'Recently'}</div>
|
|
{item.subject?.url ? <a href={item.subject.url} className="mt-3 inline-flex text-sm font-semibold text-sky-200">Open</a> : null}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function LeadershipPreview({ leadership }) {
|
|
if (!Array.isArray(leadership) || leadership.length === 0) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<section className="relative overflow-hidden rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className="absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r from-amber-400/70 via-yellow-300/40 to-transparent" />
|
|
<div className="flex items-center gap-3">
|
|
<span className="inline-flex h-10 w-10 items-center justify-center rounded-[14px] border border-amber-300/20 bg-amber-400/10 text-amber-200">
|
|
<i className="fa-solid fa-crown fa-fw" />
|
|
</span>
|
|
<div>
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-200/70">Leadership</p>
|
|
<h2 className="text-xl font-semibold text-white">Owner and admins</h2>
|
|
</div>
|
|
</div>
|
|
<div className="mt-5 grid gap-3 sm:grid-cols-2">
|
|
{leadership.map((member) => {
|
|
const roleKey = String(member.role || '').toLowerCase()
|
|
const roleStyle = MEMBER_ROLE_COLORS[roleKey] || MEMBER_ROLE_COLORS.contributor
|
|
return (
|
|
<a key={member.id} href={member.profile_url || '#'} className="group flex items-center gap-3 rounded-[24px] border border-white/10 bg-black/20 px-4 py-4 transition hover:border-white/20 hover:bg-white/[0.04]">
|
|
{member.avatar_url
|
|
? <img src={member.avatar_url} alt={member.name || member.username} className="h-12 w-12 shrink-0 rounded-2xl object-cover ring-1 ring-white/10 transition group-hover:ring-white/20" />
|
|
: <div className="flex h-12 w-12 shrink-0 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 className="min-w-0 flex-1">
|
|
<div className="truncate font-semibold text-white">{member.name || member.username}</div>
|
|
<div className={`mt-1 inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-[10px] font-semibold uppercase tracking-[0.14em] ${roleStyle.badge}`}>
|
|
<i className={`fa-solid ${roleStyle.icon} fa-fw text-[9px]`} />
|
|
{member.role_label || member.role}
|
|
</div>
|
|
</div>
|
|
</a>
|
|
)
|
|
})}
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
function FocusCard({ eyebrow, item, badgeKey = 'status', ctaLabel }) {
|
|
if (!item?.title) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/70">{eyebrow}</p>
|
|
<div className="mt-2 flex items-center gap-3">
|
|
<h2 className="text-2xl font-semibold text-white">{item.title}</h2>
|
|
{item[badgeKey] ? <span className="rounded-full border border-white/10 bg-white/[0.04] px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-300">{item[badgeKey]}</span> : null}
|
|
</div>
|
|
<p className="mt-4 text-sm leading-7 text-slate-300">{item.summary || 'Open for more details.'}</p>
|
|
<a href={item.url} className="mt-4 inline-flex rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white">{ctaLabel}</a>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
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 (
|
|
<section className="relative overflow-hidden rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className="absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r from-sky-400/70 via-cyan-300/40 to-transparent" />
|
|
<div className="flex items-center gap-3">
|
|
<span className="inline-flex h-10 w-10 items-center justify-center rounded-[14px] border border-sky-300/20 bg-sky-400/10 text-sky-200">
|
|
<i className="fa-solid fa-shield-check fa-fw" />
|
|
</span>
|
|
<div>
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/70">Trust signals</p>
|
|
<h2 className="text-xl font-semibold text-white">How this group shows up</h2>
|
|
</div>
|
|
</div>
|
|
<div className="mt-4 flex flex-wrap gap-2">
|
|
{signals.map((signal) => {
|
|
const ts = TONE_STYLES[signal.tone] || { badge: 'border-white/10 bg-white/[0.04] text-white', dot: 'bg-slate-400' }
|
|
return (
|
|
<span key={signal.key} className={`inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-[11px] font-semibold ${ts.badge}`}>
|
|
<span className={`h-1.5 w-1.5 rounded-full ${ts.dot}`} />{signal.label}
|
|
</span>
|
|
)
|
|
})}
|
|
</div>
|
|
<div className="mt-5 space-y-3">
|
|
{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 (
|
|
<div key={`${signal.key}-reason`} className="relative overflow-hidden rounded-2xl border border-white/10 bg-black/20 px-4 py-4">
|
|
<div className={`absolute inset-x-0 top-0 h-[2px] rounded-t-2xl bg-gradient-to-r ${ts.bar}`} />
|
|
<div className={`text-sm font-semibold ${ts.badge.includes('text-') ? ts.badge.split(' ').find((c) => c.startsWith('text-')) : 'text-white'}`}>{signal.label}</div>
|
|
<p className="mt-1.5 text-sm leading-6 text-slate-400">{signal.reason}</p>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
function BadgeShowcase({ badges }) {
|
|
if (!Array.isArray(badges) || badges.length === 0) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<section className="relative overflow-hidden rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className="absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r from-amber-400/70 via-yellow-300/40 to-transparent" />
|
|
<div className="flex items-center gap-3">
|
|
<span className="inline-flex h-10 w-10 items-center justify-center rounded-[14px] border border-amber-300/20 bg-amber-400/10 text-amber-200">
|
|
<i className="fa-solid fa-medal fa-fw" />
|
|
</span>
|
|
<div>
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-100/80">Badges</p>
|
|
<h2 className="text-xl font-semibold text-white">Earned group signals</h2>
|
|
</div>
|
|
</div>
|
|
<div className="mt-5 grid gap-3">
|
|
{badges.map((badge) => (
|
|
<div key={badge.key} className="relative overflow-hidden rounded-2xl border border-amber-300/10 bg-amber-400/5 px-4 py-4">
|
|
<div className="absolute inset-y-0 left-0 w-[3px] rounded-l-2xl bg-gradient-to-b from-amber-400/70 to-amber-300/20" />
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="flex items-center gap-2">
|
|
<i className="fa-solid fa-certificate text-amber-300/70" />
|
|
<div className="font-semibold text-white">{badge.label}</div>
|
|
</div>
|
|
{badge.awarded_at ? <div className="text-xs text-slate-500">{new Date(badge.awarded_at).toLocaleDateString()}</div> : null}
|
|
</div>
|
|
<p className="mt-1.5 text-sm leading-6 text-slate-400">{badge.reason}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
function ContributorHighlights({ contributors }) {
|
|
if (!Array.isArray(contributors) || contributors.length === 0) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<section className="relative overflow-hidden rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className="absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r from-emerald-400/70 via-teal-300/40 to-transparent" />
|
|
<div className="flex items-center gap-3">
|
|
<span className="inline-flex h-10 w-10 items-center justify-center rounded-[14px] border border-emerald-300/20 bg-emerald-400/10 text-emerald-200">
|
|
<i className="fa-solid fa-user-star fa-fw" />
|
|
</span>
|
|
<div>
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-emerald-200/70">Contributors</p>
|
|
<h2 className="text-xl font-semibold text-white">Trusted collaborators</h2>
|
|
</div>
|
|
</div>
|
|
<div className="mt-5 space-y-3">
|
|
{contributors.map((entry) => (
|
|
<a key={entry.user?.id} href={entry.user?.profile_url || '#'} className="group flex gap-3 rounded-[24px] border border-white/10 bg-black/20 px-4 py-4 transition hover:border-emerald-300/20 hover:bg-white/[0.04]">
|
|
{entry.user?.avatar_url
|
|
? <img src={entry.user.avatar_url} alt={entry.user?.name || entry.user?.username} className="h-12 w-12 shrink-0 rounded-2xl object-cover ring-1 ring-white/10 transition group-hover:ring-emerald-300/20" />
|
|
: <div className="flex h-12 w-12 shrink-0 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 className="min-w-0 flex-1">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<div className="truncate font-semibold text-white">{entry.user?.name || entry.user?.username}</div>
|
|
{entry.trusted_indicator ? (
|
|
<span className="inline-flex items-center gap-1 rounded-full border border-emerald-300/20 bg-emerald-300/10 px-2.5 py-0.5 text-[10px] font-semibold uppercase tracking-[0.16em] text-emerald-100">
|
|
<i className="fa-solid fa-circle-check text-[9px]" />Trusted
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
{entry.summary ? <p className="mt-1 text-sm text-slate-400">{entry.summary}</p> : null}
|
|
<div className="mt-2 flex items-center gap-3 text-xs text-slate-500">
|
|
<span><i className="fa-solid fa-rocket mr-1" />{entry.counts?.releases || 0}</span>
|
|
<span><i className="fa-solid fa-images mr-1" />{entry.counts?.credited_artworks || 0}</span>
|
|
<span><i className="fa-solid fa-diagram-project mr-1" />{entry.counts?.projects || 0}</span>
|
|
</div>
|
|
{Array.isArray(entry.badges) && entry.badges.length > 0 ? (
|
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
{entry.badges.slice(0, 3).map((badge) => (
|
|
<span key={`${entry.user?.id}-${badge.key}`} className="rounded-full border border-white/10 bg-white/[0.04] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-300">{badge.label}</span>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</a>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
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 (
|
|
<div className="relative min-h-screen overflow-hidden pb-16">
|
|
<SeoHead title={`${group.name} - Skinbase`} description={group.headline || group.bio || 'Skinbase group'} />
|
|
<div
|
|
aria-hidden="true"
|
|
className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[34rem] opacity-90"
|
|
style={{
|
|
background: 'radial-gradient(circle at top left, rgba(56,189,248,0.18), transparent 32%), radial-gradient(circle at 82% 10%, rgba(16,185,129,0.16), transparent 28%), linear-gradient(180deg, #07101d 0%, #0a1220 42%, #0a1220 100%)',
|
|
}}
|
|
/>
|
|
<div
|
|
aria-hidden="true"
|
|
className="pointer-events-none absolute inset-0 -z-10 opacity-[0.06]"
|
|
style={{ backgroundImage: 'url(/gfx/noise.png)', backgroundSize: '180px' }}
|
|
/>
|
|
|
|
<GroupHero
|
|
group={group}
|
|
recruitment={recruitment}
|
|
trustSignals={trustSignals}
|
|
following={following}
|
|
followersCount={followersCount}
|
|
currentJoinRequest={currentJoinRequest}
|
|
shareLabel={shareLabel}
|
|
onToggleFollow={toggleFollow}
|
|
onJoinRequest={submitJoinRequest}
|
|
onWithdrawJoinRequest={withdrawJoinRequest}
|
|
onShare={handleShare}
|
|
onReport={submitReport}
|
|
reportEndpoint={props.reportEndpoint}
|
|
/>
|
|
|
|
<div className="mt-6">
|
|
<GroupTabs baseUrl={group.urls?.public || '/groups'} activeSection={section} />
|
|
</div>
|
|
|
|
<div className={`${contentShellClassName} pt-6`}>
|
|
|
|
{section === 'overview' ? (
|
|
<div className="mt-8 grid gap-8">
|
|
<section className="relative overflow-hidden rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className="absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r from-amber-400/70 via-yellow-300/40 to-transparent" />
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="flex items-center gap-3">
|
|
<span className="inline-flex h-10 w-10 items-center justify-center rounded-[14px] border border-amber-300/20 bg-amber-400/10 text-amber-200">
|
|
<i className="fa-solid fa-star fa-fw" />
|
|
</span>
|
|
<div>
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-200/70">Highlights</p>
|
|
<h2 className="text-xl font-semibold text-white">Featured artworks</h2>
|
|
</div>
|
|
</div>
|
|
<a href={`${group.urls?.public}/artworks`} className="text-sm font-semibold text-sky-200 transition hover:text-sky-100">Browse all</a>
|
|
</div>
|
|
<ArtworkGrid artworks={featuredArtworks} emptyLabel="No featured artworks yet." />
|
|
</section>
|
|
|
|
<section className="relative overflow-hidden rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className="absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r from-sky-400/70 via-cyan-300/40 to-transparent" />
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="flex items-center gap-3">
|
|
<span className="inline-flex h-10 w-10 items-center justify-center rounded-[14px] border border-sky-300/20 bg-sky-400/10 text-sky-200">
|
|
<i className="fa-solid fa-images fa-fw" />
|
|
</span>
|
|
<div>
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/70">Latest work</p>
|
|
<h2 className="text-xl font-semibold text-white">Latest artworks</h2>
|
|
</div>
|
|
</div>
|
|
<a href={`${group.urls?.public}/artworks`} className="text-sm font-semibold text-sky-200 transition hover:text-sky-100">View archive</a>
|
|
</div>
|
|
<ArtworkGrid artworks={artworks.slice(0, 6)} emptyLabel="No published artworks yet." />
|
|
</section>
|
|
|
|
<section className="relative overflow-hidden rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className="absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r from-violet-400/70 via-sky-300/40 to-transparent" />
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="flex items-center gap-3">
|
|
<span className="inline-flex h-10 w-10 items-center justify-center rounded-[14px] border border-violet-300/20 bg-violet-400/10 text-violet-200">
|
|
<i className="fa-solid fa-rocket fa-fw" />
|
|
</span>
|
|
<div>
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-violet-200/70">Pipeline</p>
|
|
<h2 className="text-xl font-semibold text-white">Recent releases</h2>
|
|
</div>
|
|
</div>
|
|
<a href={`${group.urls?.public}/releases`} className="text-sm font-semibold text-sky-200 transition hover:text-sky-100">View releases</a>
|
|
</div>
|
|
<ReleaseGrid releases={releases.slice(0, 3)} emptyLabel="No public releases yet." />
|
|
</section>
|
|
|
|
<div className="grid gap-8 xl:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
|
|
<section className="relative overflow-hidden rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className="absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r from-sky-400/70 via-cyan-300/40 to-transparent" />
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="flex items-center gap-3">
|
|
<span className="inline-flex h-10 w-10 items-center justify-center rounded-[14px] border border-sky-300/20 bg-sky-400/10 text-sky-200">
|
|
<i className="fa-solid fa-layer-group fa-fw" />
|
|
</span>
|
|
<div>
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/70">Curated</p>
|
|
<h2 className="text-xl font-semibold text-white">Featured collections</h2>
|
|
</div>
|
|
</div>
|
|
<a href={`${group.urls?.public}/collections`} className="text-sm font-semibold text-sky-200 transition hover:text-sky-100">View collections</a>
|
|
</div>
|
|
<CollectionGrid collections={featuredCollections.length > 0 ? featuredCollections : collections.slice(0, 2)} emptyLabel="No featured collections yet." />
|
|
</section>
|
|
|
|
<div className="grid gap-8">
|
|
{group.pinned_post ? (
|
|
<section className="relative overflow-hidden rounded-[30px] border border-amber-300/15 bg-amber-400/5 p-6">
|
|
<div className="absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r from-amber-400/80 to-transparent" />
|
|
<div className="flex items-center gap-3">
|
|
<span className="inline-flex h-9 w-9 items-center justify-center rounded-[14px] border border-amber-300/20 bg-amber-400/10 text-amber-200">
|
|
<i className="fa-solid fa-thumbtack fa-fw text-sm" />
|
|
</span>
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-100/80">Pinned post</p>
|
|
</div>
|
|
<h2 className="mt-3 text-xl font-semibold text-white">{group.pinned_post.title}</h2>
|
|
<p className="mt-3 text-sm leading-7 text-amber-50/80">{group.pinned_post.excerpt || 'Read the latest pinned update from this group.'}</p>
|
|
<a href={group.pinned_post.url} className="mt-4 inline-flex items-center gap-2 rounded-2xl border border-amber-300/20 bg-amber-400/10 px-4 py-2 text-sm font-semibold text-amber-100 transition hover:bg-amber-400/15"><i className="fa-solid fa-book-open fa-fw" />Read post</a>
|
|
</section>
|
|
) : null}
|
|
|
|
<FocusCard eyebrow="Releases" item={group.featured_release} badgeKey="current_stage" ctaLabel="Open release" />
|
|
<FocusCard eyebrow="Projects" item={group.featured_project} ctaLabel="Open project" />
|
|
<FocusCard eyebrow="Challenges" item={group.active_challenge} ctaLabel="View challenge" />
|
|
<FocusCard eyebrow="Events" item={group.upcoming_event} badgeKey="event_type" ctaLabel="View event" />
|
|
<LeadershipPreview leadership={leadership} />
|
|
<TrustSignalPanel signals={trustSignals} />
|
|
<BadgeShowcase badges={badgeShowcase} />
|
|
<ContributorHighlights contributors={topContributors.slice(0, 4)} />
|
|
|
|
{recruitment?.is_recruiting ? (
|
|
<section className="rounded-[30px] border border-emerald-300/20 bg-emerald-400/10 p-6">
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-emerald-100/80">Recruiting</p>
|
|
<h2 className="mt-2 text-2xl font-semibold text-white">{recruitment.headline || `${group.name} is looking for collaborators`}</h2>
|
|
<p className="mt-4 text-sm leading-7 text-emerald-50/90">{recruitment.description || 'This group is currently open to new contributors.'}</p>
|
|
{Array.isArray(recruitment.roles) && recruitment.roles.length > 0 ? <div className="mt-4 flex flex-wrap gap-2">{recruitment.roles.map((role) => <span key={role} className="rounded-full border border-white/10 bg-white/[0.08] px-3 py-1.5 text-xs font-semibold text-white">{role}</span>)}</div> : null}
|
|
</section>
|
|
) : null}
|
|
|
|
<section className="relative overflow-hidden rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className="absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r from-teal-400/70 to-transparent" />
|
|
<div className="flex items-center gap-3">
|
|
<span className="inline-flex h-9 w-9 items-center justify-center rounded-[14px] border border-teal-300/20 bg-teal-400/10 text-teal-200">
|
|
<i className="fa-solid fa-download fa-fw text-sm" />
|
|
</span>
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-teal-200/70">Resources</p>
|
|
</div>
|
|
<h2 className="mt-2 text-xl font-semibold text-white">Shared downloads</h2>
|
|
<AssetGrid assets={assets.slice(0, 3)} />
|
|
</section>
|
|
|
|
<section className="relative overflow-hidden rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className="absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r from-sky-400/70 to-transparent" />
|
|
<div className="flex items-center gap-3">
|
|
<span className="inline-flex h-9 w-9 items-center justify-center rounded-[14px] border border-sky-300/20 bg-sky-400/10 text-sky-200">
|
|
<i className="fa-solid fa-bolt fa-fw text-sm" />
|
|
</span>
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/70">Public feed</p>
|
|
</div>
|
|
<h2 className="mt-2 text-xl font-semibold text-white">Recent activity</h2>
|
|
<ActivityFeed items={activity.slice(0, 4)} />
|
|
</section>
|
|
|
|
<section className="relative overflow-hidden rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className="absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r from-slate-400/50 to-transparent" />
|
|
<div className="flex items-center gap-3">
|
|
<span className="inline-flex h-9 w-9 items-center justify-center rounded-[14px] border border-white/10 bg-white/[0.05] text-slate-300">
|
|
<i className="fa-solid fa-id-card fa-fw text-sm" />
|
|
</span>
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400/80">About</p>
|
|
</div>
|
|
<h2 className="mt-2 text-xl font-semibold text-white">About {group.name}</h2>
|
|
<p className="mt-4 text-sm leading-7 text-slate-300">{group.bio || 'No long-form description yet.'}</p>
|
|
<div className="mt-4 flex flex-wrap gap-3 text-xs text-slate-400">
|
|
{group.founded_at ? <span className="inline-flex items-center gap-1.5"><i className="fa-solid fa-calendar-days text-slate-500" />Founded {new Date(group.founded_at).toLocaleDateString()}</span> : null}
|
|
{group.type ? <span className="inline-flex items-center gap-1.5"><i className="fa-solid fa-tag text-slate-500" />{group.type}</span> : null}
|
|
{group.website_url ? <a href={group.website_url} className="inline-flex items-center gap-1.5 text-sky-200 underline underline-offset-4 transition hover:text-sky-100"><i className="fa-solid fa-link" />Website</a> : null}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{section === 'artworks' ? (
|
|
<section className="mt-8 relative overflow-hidden rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className="absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r from-sky-400/70 via-cyan-300/40 to-transparent" />
|
|
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<span className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-[14px] border border-sky-300/20 bg-sky-400/10 text-sky-200">
|
|
<i className="fa-solid fa-images fa-fw" />
|
|
</span>
|
|
<div>
|
|
<h2 className="text-2xl font-semibold text-white">Artworks</h2>
|
|
<p className="mt-1 text-sm text-slate-400">Filter the group archive by title or contributor credit label, then change the sort order.</p>
|
|
</div>
|
|
</div>
|
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
<label className="grid gap-2 text-sm text-slate-300">
|
|
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Search</span>
|
|
<input value={artworkQuery} onChange={(event) => setArtworkQuery(event.target.value)} placeholder="Filter artworks" className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
|
|
</label>
|
|
<label className="grid gap-2 text-sm text-slate-300">
|
|
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Sort</span>
|
|
<select value={artworkSort} onChange={(event) => setArtworkSort(event.target.value)} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none">
|
|
<option value="latest">Latest first</option>
|
|
<option value="oldest">Oldest first</option>
|
|
<option value="title">Title A-Z</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<ArtworkGrid artworks={filteredArtworks} emptyLabel="No published artworks match the current filter." />
|
|
</section>
|
|
) : null}
|
|
|
|
{section === 'collections' ? (
|
|
<section className="mt-8 relative overflow-hidden rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className="absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r from-sky-400/70 via-cyan-300/40 to-transparent" />
|
|
<div className="flex items-center gap-3">
|
|
<span className="inline-flex h-10 w-10 items-center justify-center rounded-[14px] border border-sky-300/20 bg-sky-400/10 text-sky-200">
|
|
<i className="fa-solid fa-layer-group fa-fw" />
|
|
</span>
|
|
<h2 className="text-2xl font-semibold text-white">Collections</h2>
|
|
</div>
|
|
<CollectionGrid collections={collections} emptyLabel="No collections yet." />
|
|
</section>
|
|
) : null}
|
|
|
|
{section === 'posts' ? (
|
|
<div className="mt-8">
|
|
<div className="mb-6 flex items-center gap-4">
|
|
<span className="inline-flex h-10 w-10 items-center justify-center rounded-[14px] border border-sky-300/20 bg-sky-400/10 text-sky-200">
|
|
<i className="fa-solid fa-newspaper fa-fw" />
|
|
</span>
|
|
<div>
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/70">Group updates</p>
|
|
<h2 className="text-2xl font-semibold text-white">Posts</h2>
|
|
</div>
|
|
</div>
|
|
{posts.length > 0 ? (
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
{posts.map((post) => {
|
|
const typeKey = String(post.type || '').toLowerCase()
|
|
const typeStyle = POST_TYPE_ICONS[typeKey] || POST_TYPE_ICONS.announcement
|
|
return (
|
|
<a key={post.id} href={post.url} className="group relative overflow-hidden rounded-[28px] border border-white/10 bg-black/20 p-5 transition hover:border-white/20 hover:shadow-[0_8px_24px_rgba(2,6,23,0.4)]">
|
|
<div className={`absolute inset-x-0 top-0 h-[3px] rounded-t-[28px] bg-gradient-to-r ${typeStyle.bar}`} />
|
|
<div className="flex items-start gap-4">
|
|
<span className={`shrink-0 inline-flex h-10 w-10 items-center justify-center rounded-[14px] border ${typeStyle.border} ${typeStyle.bg} ${typeStyle.text}`}>
|
|
<i className={`fa-solid ${typeStyle.icon} fa-fw`} />
|
|
</span>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className={`text-[10px] font-semibold uppercase tracking-[0.18em] ${typeStyle.text}`}>{post.type || 'post'}</span>
|
|
{post.is_pinned ? (
|
|
<span className="inline-flex items-center gap-1 rounded-full border border-amber-300/20 bg-amber-400/10 px-2.5 py-0.5 text-[10px] font-semibold uppercase tracking-[0.16em] text-amber-100">
|
|
<i className="fa-solid fa-thumbtack text-[9px]" />Pinned
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
<h3 className="mt-1.5 text-base font-semibold text-white">{post.title}</h3>
|
|
<p className="mt-1.5 text-sm leading-6 text-slate-300">{post.excerpt || 'Open the post to read more.'}</p>
|
|
</div>
|
|
</div>
|
|
<div className="mt-3 flex justify-end">
|
|
<span className="text-xs font-semibold text-sky-300 opacity-0 transition group-hover:opacity-100">Read post <i className="fa-solid fa-arrow-right ml-0.5" /></span>
|
|
</div>
|
|
</a>
|
|
)
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col items-center gap-4 rounded-[32px] border border-white/8 bg-white/[0.02] py-16 text-center">
|
|
<span className="inline-flex h-16 w-16 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.04] text-slate-500">
|
|
<i className="fa-solid fa-newspaper text-3xl" />
|
|
</span>
|
|
<div>
|
|
<p className="text-sm font-semibold text-slate-300">No posts published yet.</p>
|
|
<p className="mt-1 text-xs text-slate-500">Check back later for group updates and announcements.</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : null}
|
|
|
|
{section === 'projects' ? (
|
|
<section className="mt-8 relative overflow-hidden rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className="absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r from-violet-400/70 via-sky-300/40 to-transparent" />
|
|
<div className="flex items-center gap-3">
|
|
<span className="inline-flex h-10 w-10 items-center justify-center rounded-[14px] border border-violet-300/20 bg-violet-400/10 text-violet-200">
|
|
<i className="fa-solid fa-diagram-project fa-fw" />
|
|
</span>
|
|
<div>
|
|
<h2 className="text-2xl font-semibold text-white">Projects</h2>
|
|
<p className="mt-1 text-sm text-slate-400">Structured releases, collaboration hubs, and production pages published by this group.</p>
|
|
</div>
|
|
</div>
|
|
<CompactCardGrid items={projects} emptyLabel="No public projects yet." />
|
|
</section>
|
|
) : null}
|
|
|
|
{section === 'releases' ? (
|
|
<section className="mt-8 relative overflow-hidden rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className="absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r from-sky-400/70 via-cyan-300/40 to-transparent" />
|
|
<div className="flex items-center gap-3">
|
|
<span className="inline-flex h-10 w-10 items-center justify-center rounded-[14px] border border-sky-300/20 bg-sky-400/10 text-sky-200">
|
|
<i className="fa-solid fa-rocket fa-fw" />
|
|
</span>
|
|
<div>
|
|
<h2 className="text-2xl font-semibold text-white">Releases</h2>
|
|
<p className="mt-1 text-sm text-slate-400">Published drops, milestone pipelines, and linked showcases from this group.</p>
|
|
</div>
|
|
</div>
|
|
<ReleaseGrid releases={releases} emptyLabel="No public releases yet." />
|
|
</section>
|
|
) : null}
|
|
|
|
{section === 'challenges' ? (
|
|
<section className="mt-8 relative overflow-hidden rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className="absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r from-amber-400/70 via-yellow-300/40 to-transparent" />
|
|
<div className="flex items-center gap-3">
|
|
<span className="inline-flex h-10 w-10 items-center justify-center rounded-[14px] border border-amber-300/20 bg-amber-400/10 text-amber-200">
|
|
<i className="fa-solid fa-trophy fa-fw" />
|
|
</span>
|
|
<div>
|
|
<h2 className="text-2xl font-semibold text-white">Challenges</h2>
|
|
<p className="mt-1 text-sm text-slate-400">Current and past prompts, internal sprints, and public-facing challenge runs.</p>
|
|
</div>
|
|
</div>
|
|
<CompactCardGrid items={challenges} emptyLabel="No public challenges yet." />
|
|
</section>
|
|
) : null}
|
|
|
|
{section === 'events' ? (
|
|
<section className="mt-8 relative overflow-hidden rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className="absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r from-violet-400/70 via-sky-300/40 to-transparent" />
|
|
<div className="flex items-center gap-3">
|
|
<span className="inline-flex h-10 w-10 items-center justify-center rounded-[14px] border border-violet-300/20 bg-violet-400/10 text-violet-200">
|
|
<i className="fa-solid fa-calendar-days fa-fw" />
|
|
</span>
|
|
<div>
|
|
<h2 className="text-2xl font-semibold text-white">Events</h2>
|
|
<p className="mt-1 text-sm text-slate-400">Launches, milestones, streams, and other moments on the group timeline.</p>
|
|
</div>
|
|
</div>
|
|
<CompactCardGrid items={events} emptyLabel="No public events yet." badgeKey="event_type" />
|
|
</section>
|
|
) : null}
|
|
|
|
{section === 'activity' ? (
|
|
<section className="mt-8 relative overflow-hidden rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className="absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r from-sky-400/70 via-cyan-300/40 to-transparent" />
|
|
<div className="flex items-center gap-3">
|
|
<span className="inline-flex h-10 w-10 items-center justify-center rounded-[14px] border border-sky-300/20 bg-sky-400/10 text-sky-200">
|
|
<i className="fa-solid fa-bolt fa-fw" />
|
|
</span>
|
|
<div>
|
|
<h2 className="text-2xl font-semibold text-white">Activity</h2>
|
|
<p className="mt-1 text-sm text-slate-400">Public milestones from posts, releases, events, member changes, and challenge highlights.</p>
|
|
</div>
|
|
</div>
|
|
<ActivityFeed items={activity} />
|
|
</section>
|
|
) : null}
|
|
|
|
{section === 'members' ? (
|
|
<div className="mt-8">
|
|
<div className="mb-6 flex items-center gap-4">
|
|
<span className="inline-flex h-10 w-10 items-center justify-center rounded-[14px] border border-sky-300/20 bg-sky-400/10 text-sky-200">
|
|
<i className="fa-solid fa-users fa-fw" />
|
|
</span>
|
|
<div>
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/70">Community</p>
|
|
<h2 className="text-2xl font-semibold text-white">Members</h2>
|
|
</div>
|
|
</div>
|
|
<div className="grid gap-6">
|
|
{[
|
|
['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 (
|
|
<section key={label} className="relative overflow-hidden rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className={`absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r ${roleKey === 'owner' ? 'from-amber-400/70 to-transparent' : roleKey === 'admin' ? 'from-sky-400/70 to-transparent' : roleKey === 'editor' ? 'from-violet-400/70 to-transparent' : 'from-emerald-400/70 to-transparent'}`} />
|
|
<div className="flex items-center gap-3">
|
|
<span className={`inline-flex h-8 w-8 items-center justify-center rounded-xl border ${roleStyle.badge}`}>
|
|
<i className={`fa-solid ${roleStyle.icon} fa-fw text-sm ${roleStyle.iconColor}`} />
|
|
</span>
|
|
<h3 className="text-lg font-semibold text-white">{label}</h3>
|
|
<span className="ml-auto rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300">{bucket.length}</span>
|
|
</div>
|
|
<div className="mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
|
{bucket.map((member) => (
|
|
<a key={member.id} href={member.user?.profile_url || '#'} className="group flex items-center gap-3 rounded-[24px] border border-white/10 bg-black/20 px-4 py-4 transition hover:border-white/20 hover:bg-white/[0.04]">
|
|
{member.user?.avatar_url
|
|
? <img src={member.user.avatar_url} alt={member.user.name || member.user.username} className="h-12 w-12 shrink-0 rounded-2xl object-cover ring-1 ring-white/10 transition group-hover:ring-white/20" />
|
|
: <div className="flex h-12 w-12 shrink-0 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 className="min-w-0">
|
|
<div className="truncate font-semibold text-white">{member.user?.name || member.user?.username}</div>
|
|
<div className={`mt-1 inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-[10px] font-semibold uppercase tracking-[0.14em] ${roleStyle.badge}`}>
|
|
<i className={`fa-solid ${roleStyle.icon} fa-fw text-[9px]`} />
|
|
{member.role_label || member.role}
|
|
</div>
|
|
</div>
|
|
</a>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{section === 'about' ? (
|
|
<section className="mt-8 relative overflow-hidden rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className="absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r from-slate-400/50 to-transparent" />
|
|
<div className="flex items-center gap-3">
|
|
<span className="inline-flex h-10 w-10 items-center justify-center rounded-[14px] border border-white/10 bg-white/[0.05] text-slate-300">
|
|
<i className="fa-solid fa-id-card fa-fw" />
|
|
</span>
|
|
<h2 className="text-2xl font-semibold text-white">About</h2>
|
|
</div>
|
|
<div className="mt-5 space-y-4 text-sm leading-7 text-slate-300">
|
|
<p>{group.bio || 'No long-form description yet.'}</p>
|
|
{group.website_url ? <p><a href={group.website_url} className="inline-flex items-center gap-1.5 text-sky-200 underline underline-offset-4 transition hover:text-sky-100"><i className="fa-solid fa-link" />{group.website_url}</a></p> : null}
|
|
{Array.isArray(group.links) && group.links.length > 0 ? (
|
|
<div className="flex flex-wrap gap-3">
|
|
{group.links.map((link) => (
|
|
<a key={`${link.label}-${link.url}`} href={link.url} className="inline-flex items-center gap-2 rounded-[14px] border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white transition hover:bg-white/[0.08]">
|
|
<i className="fa-solid fa-arrow-up-right-from-square text-slate-400" />{link.label}
|
|
</a>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
<div className="mt-4 flex flex-wrap gap-4 border-t border-white/8 pt-4 text-xs text-slate-400">
|
|
{group.founded_at ? <span className="inline-flex items-center gap-2"><i className="fa-solid fa-calendar-days text-slate-500" />Founded {new Date(group.founded_at).toLocaleDateString()}</span> : null}
|
|
{group.type ? <span className="inline-flex items-center gap-2"><i className="fa-solid fa-tag text-slate-500" />{group.type}</span> : null}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
)
|
|
} |