Commit workspace changes
This commit is contained in:
63
resources/js/Pages/Group/GroupChallengeShow.jsx
Normal file
63
resources/js/Pages/Group/GroupChallengeShow.jsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react'
|
||||
import { usePage } from '@inertiajs/react'
|
||||
import SeoHead from '../../components/seo/SeoHead'
|
||||
|
||||
export default function GroupChallengeShow() {
|
||||
const { props } = usePage()
|
||||
const group = props.group || {}
|
||||
const challenge = props.challenge || {}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(234,179,8,0.15),_transparent_28%),linear-gradient(180deg,_#020617_0%,_#02040a_100%)] px-4 py-10 sm:px-6 lg:px-8">
|
||||
<SeoHead seo={props.seo || {}} title={`${challenge.title || group.name} - Skinbase`} description={challenge.summary || challenge.description || 'Group challenge'} />
|
||||
<div className="mx-auto max-w-6xl space-y-8">
|
||||
<section className="overflow-hidden rounded-[32px] border border-white/10 bg-white/[0.03]">
|
||||
{challenge.cover_url ? <img src={challenge.cover_url} alt={challenge.title} className="h-56 w-full object-cover" /> : <div className="h-40 bg-white/[0.03]" />}
|
||||
<div className="p-6">
|
||||
<a href={group.urls?.public} className="text-sm font-semibold text-amber-200">{group.name}</a>
|
||||
<h1 className="mt-4 text-4xl font-semibold text-white">{challenge.title}</h1>
|
||||
<p className="mt-4 max-w-3xl text-sm leading-7 text-slate-300">{challenge.summary || challenge.description || 'Group challenge'}</p>
|
||||
<div className="mt-5 flex flex-wrap gap-3 text-xs uppercase tracking-[0.16em] text-slate-400">
|
||||
<span>{challenge.status}</span>
|
||||
<span>{challenge.visibility}</span>
|
||||
<span>{String(challenge.participation_scope || '').replace('_', ' ')}</span>
|
||||
{challenge.start_at ? <span>Starts {new Date(challenge.start_at).toLocaleDateString()}</span> : null}
|
||||
{challenge.end_at ? <span>Ends {new Date(challenge.end_at).toLocaleDateString()}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="grid gap-8 xl:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]">
|
||||
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-2xl font-semibold text-white">Challenge brief</h2>
|
||||
<p className="mt-4 text-sm leading-7 text-slate-300">{challenge.description || 'No extended challenge brief yet.'}</p>
|
||||
{challenge.rules_text ? (
|
||||
<div className="mt-6 rounded-2xl border border-white/10 bg-black/20 p-4">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Rules</div>
|
||||
<p className="mt-2 text-sm leading-7 text-slate-300">{challenge.rules_text}</p>
|
||||
</div>
|
||||
) : null}
|
||||
{challenge.submission_instructions ? (
|
||||
<div className="mt-6 rounded-2xl border border-white/10 bg-black/20 p-4">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Submission instructions</div>
|
||||
<p className="mt-2 text-sm leading-7 text-slate-300">{challenge.submission_instructions}</p>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-2xl font-semibold text-white">Entries</h2>
|
||||
<div className="mt-4 grid gap-4 sm:grid-cols-2 xl:grid-cols-1">
|
||||
{Array.isArray(challenge.artworks) && challenge.artworks.length > 0 ? challenge.artworks.map((artwork) => (
|
||||
<a key={artwork.id} href={artwork.url} className="overflow-hidden rounded-[24px] border border-white/10 bg-black/20">
|
||||
{artwork.thumb ? <img src={artwork.thumb} alt={artwork.title} className="aspect-[4/3] w-full object-cover" /> : null}
|
||||
<div className="p-4 text-white">{artwork.title}</div>
|
||||
</a>
|
||||
)) : <p className="text-sm text-slate-400">No entries linked yet.</p>}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
42
resources/js/Pages/Group/GroupEventShow.jsx
Normal file
42
resources/js/Pages/Group/GroupEventShow.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
import { usePage } from '@inertiajs/react'
|
||||
import SeoHead from '../../components/seo/SeoHead'
|
||||
|
||||
export default function GroupEventShow() {
|
||||
const { props } = usePage()
|
||||
const group = props.group || {}
|
||||
const event = props.event || {}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(16,185,129,0.15),_transparent_28%),linear-gradient(180deg,_#020617_0%,_#02040a_100%)] px-4 py-10 sm:px-6 lg:px-8">
|
||||
<SeoHead seo={props.seo || {}} title={`${event.title || group.name} - Skinbase`} description={event.summary || event.description || 'Group event'} />
|
||||
<div className="mx-auto max-w-5xl space-y-8">
|
||||
<section className="overflow-hidden rounded-[32px] border border-white/10 bg-white/[0.03]">
|
||||
{event.cover_url ? <img src={event.cover_url} alt={event.title} className="h-56 w-full object-cover" /> : <div className="h-40 bg-white/[0.03]" />}
|
||||
<div className="p-6">
|
||||
<a href={group.urls?.public} className="text-sm font-semibold text-emerald-200">{group.name}</a>
|
||||
<h1 className="mt-4 text-4xl font-semibold text-white">{event.title}</h1>
|
||||
<p className="mt-4 max-w-3xl text-sm leading-7 text-slate-300">{event.summary || event.description || 'Group event'}</p>
|
||||
<div className="mt-5 grid gap-3 sm:grid-cols-2">
|
||||
<div className="rounded-2xl border border-white/10 bg-black/20 p-4 text-sm text-slate-300">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Starts</div>
|
||||
<div className="mt-2 text-white">{event.start_at ? new Date(event.start_at).toLocaleString() : 'Not scheduled'}</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/10 bg-black/20 p-4 text-sm text-slate-300">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Details</div>
|
||||
<div className="mt-2 text-white">{event.event_type} • {event.visibility}</div>
|
||||
{event.location ? <div className="mt-2">{event.location}</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
{event.external_url ? <a href={event.external_url} className="mt-5 inline-flex rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white">Open external link</a> : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-2xl font-semibold text-white">About this event</h2>
|
||||
<p className="mt-4 text-sm leading-7 text-slate-300">{event.description || 'No extended event details yet.'}</p>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
215
resources/js/Pages/Group/GroupFaqPage.jsx
Normal file
215
resources/js/Pages/Group/GroupFaqPage.jsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import React, { useState } from 'react'
|
||||
import { usePage } from '@inertiajs/react'
|
||||
import DocsCallout from '../../components/docs/DocsCallout'
|
||||
import DocsFaqAccordion from '../../components/docs/DocsFaqAccordion'
|
||||
import DocsSection from '../../components/docs/DocsSection'
|
||||
import DocsSidebarNav from '../../components/docs/DocsSidebarNav'
|
||||
import FaqSearchInput from '../../components/docs/FaqSearchInput'
|
||||
import QuickstartNextSteps from '../../components/docs/QuickstartNextSteps'
|
||||
import SeoHead from '../../components/seo/SeoHead'
|
||||
import { FAQ_CATEGORIES, RELATED_HELP_ITEMS } from './groupFaqContent'
|
||||
|
||||
function HeroStat({ label, value, note }) {
|
||||
return (
|
||||
<div className="rounded-[22px] border border-white/10 bg-black/20 p-4">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">{label}</div>
|
||||
<div className="mt-2 text-lg font-semibold text-white">{value}</div>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-400">{note}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FaqAnswer({ item, links }) {
|
||||
return (
|
||||
<>
|
||||
{Array.isArray(item.paragraphs) ? item.paragraphs.map((paragraph) => (
|
||||
<p key={paragraph}>{paragraph}</p>
|
||||
)) : null}
|
||||
{Array.isArray(item.bullets) && item.bullets.length > 0 ? (
|
||||
<ul className="space-y-2">
|
||||
{item.bullets.map((bullet) => (
|
||||
<li key={bullet} className="flex gap-3">
|
||||
<span className="mt-2 h-2 w-2 shrink-0 rounded-full bg-sky-300" />
|
||||
<span>{bullet}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
{Array.isArray(item.example) && item.example.length > 0 ? (
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
{item.example.map((entry) => (
|
||||
<div key={entry.label} className="rounded-[20px] border border-white/10 bg-white/[0.03] p-3">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">{entry.label}</div>
|
||||
<div className="mt-2 text-sm font-semibold text-white">{entry.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{Array.isArray(item.links) && item.links.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-3 pt-1">
|
||||
{item.links.map((link) => (
|
||||
<a key={link.label} href={links[link.linkKey] || '#'} className="text-sm font-semibold text-sky-200 underline underline-offset-4">
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function GroupFaqPage() {
|
||||
const { props } = usePage()
|
||||
const links = props.links || {}
|
||||
const [query, setQuery] = useState('')
|
||||
const normalizedQuery = query.trim().toLowerCase()
|
||||
|
||||
const visibleCategories = FAQ_CATEGORIES.map((category) => {
|
||||
const items = category.items.filter((item) => {
|
||||
if (!normalizedQuery) return true
|
||||
|
||||
const haystack = [
|
||||
item.question,
|
||||
...(item.paragraphs || []),
|
||||
...(item.bullets || []),
|
||||
...(item.example || []).flatMap((entry) => [entry.label, entry.value]),
|
||||
].join(' ').toLowerCase()
|
||||
|
||||
return haystack.includes(normalizedQuery)
|
||||
})
|
||||
|
||||
return {
|
||||
...category,
|
||||
items,
|
||||
}
|
||||
}).filter((category) => category.items.length > 0)
|
||||
|
||||
const visibleQuestionCount = visibleCategories.reduce((total, category) => total + category.items.length, 0)
|
||||
const relatedHelpItems = RELATED_HELP_ITEMS.map((item) => ({
|
||||
...item,
|
||||
href: links[item.linkKey] || '#',
|
||||
}))
|
||||
|
||||
const jsonLd = [
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: FAQ_CATEGORIES.flatMap((category) => category.items).map((item) => ({
|
||||
'@type': 'Question',
|
||||
name: item.question,
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: (item.paragraphs || []).join(' '),
|
||||
},
|
||||
})),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(14,165,233,0.18),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(250,204,21,0.14),_transparent_22%),linear-gradient(180deg,_#020617_0%,_#030712_100%)] px-4 py-8 sm:px-6 lg:px-8">
|
||||
<SeoHead seo={props.seo || {}} title={props.title} description={props.description} jsonLd={jsonLd} />
|
||||
|
||||
<div className="mx-auto max-w-[1450px]">
|
||||
<section id="introduction" className="rounded-[36px] border border-white/10 bg-[linear-gradient(135deg,rgba(15,23,42,0.92),rgba(15,23,42,0.72)),radial-gradient(circle_at_top_right,rgba(125,211,252,0.16),transparent_28%)] p-6 shadow-[0_30px_100px_rgba(2,6,23,0.35)] md:p-8 lg:p-10">
|
||||
<div className="grid gap-8 xl:grid-cols-[minmax(0,1fr)_340px]">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80">Groups FAQ</p>
|
||||
<h1 className="mt-3 max-w-4xl text-4xl font-semibold tracking-[-0.04em] text-white md:text-5xl xl:text-6xl">Find quick answers about Groups without digging through the full guide.</h1>
|
||||
<p className="mt-5 max-w-3xl text-base leading-8 text-slate-300 md:text-lg">This page answers the most common practical questions about Groups, roles, publishing, contributor credit, invites, workflows, and troubleshooting. Use it when you want fast answers first, then go deeper only if you need to.</p>
|
||||
<div className="mt-6 flex flex-wrap gap-3">
|
||||
<a href={links.full_documentation} className="rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100 transition hover:border-sky-300/40 hover:bg-sky-300/18">Read full Groups documentation</a>
|
||||
<a href={links.quickstart} className="rounded-full border border-white/10 bg-white/[0.04] px-5 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.07]">Open Groups Quickstart</a>
|
||||
<a href={links.group_studio} className="rounded-full border border-white/10 bg-black/20 px-5 py-3 text-sm font-semibold text-slate-200 transition hover:border-white/20 hover:bg-white/[0.05]">Open Group Studio</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-3 xl:grid-cols-1">
|
||||
<HeroStat label="Best for" value="Fast practical questions" note="Use the FAQ when you need answers quickly instead of reading the longer guide front to back." />
|
||||
<HeroStat label="Core idea" value="Shared identity, preserved credit" note="Groups publish together under one identity, but the people behind the work still matter and stay visible." />
|
||||
<HeroStat label="If you need more" value="Jump deeper anytime" note="This page links back to the quickstart, the full guide, Group Studio, and the creation flow." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 max-w-3xl">
|
||||
<FaqSearchInput
|
||||
value={query}
|
||||
onChange={setQuery}
|
||||
onClear={() => setQuery('')}
|
||||
resultCount={visibleQuestionCount}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="mt-8 grid gap-6 lg:grid-cols-[240px_minmax(0,1fr)] xl:grid-cols-[240px_minmax(0,1fr)_280px]">
|
||||
<DocsSidebarNav sections={visibleCategories.map((category) => ({ id: category.id, label: category.label }))} />
|
||||
|
||||
<div className="space-y-6">
|
||||
<DocsCallout tone="note" title="How to use this page">
|
||||
Start with the category closest to your problem. If you only need the fastest route to first success, use the quickstart. If you need broader reference or advanced workflows, open the full Groups guide.
|
||||
</DocsCallout>
|
||||
|
||||
{visibleCategories.length === 0 ? (
|
||||
<section className="rounded-[32px] border border-white/10 bg-white/[0.03] p-6 shadow-[0_22px_70px_rgba(2,6,23,0.22)] md:p-7">
|
||||
<h2 className="text-2xl font-semibold text-white">No matching questions</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-slate-300">Try a broader search term like roles, invite, publish, contributor, review, or Studio.</p>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{visibleCategories.map((category) => (
|
||||
<DocsSection
|
||||
key={category.id}
|
||||
id={category.id}
|
||||
eyebrow="FAQ category"
|
||||
title={category.title}
|
||||
summary={category.summary}
|
||||
>
|
||||
<DocsFaqAccordion items={category.items} renderAnswer={(item) => <FaqAnswer item={item} links={links} />} />
|
||||
</DocsSection>
|
||||
))}
|
||||
|
||||
<DocsSection
|
||||
id="related-help"
|
||||
eyebrow="Related help"
|
||||
title="Need the next step, not just the answer?"
|
||||
summary="Use these links when the FAQ has answered the question and you are ready to act, learn more, or get support."
|
||||
>
|
||||
<QuickstartNextSteps items={relatedHelpItems} />
|
||||
|
||||
<div className="mt-6 grid gap-4 md:grid-cols-2">
|
||||
<a href={links.contact_support} className="rounded-[28px] border border-white/10 bg-black/20 p-5 transition hover:border-white/20 hover:bg-white/[0.05]">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Contact</div>
|
||||
<div className="mt-2 text-lg font-semibold text-white">Contact support</div>
|
||||
<p className="mt-3 text-sm leading-6 text-slate-300">Use this if your question is not answered here or if you need help with an account or workflow issue.</p>
|
||||
</a>
|
||||
<a href={links.report_issue} className="rounded-[28px] border border-white/10 bg-black/20 p-5 transition hover:border-white/20 hover:bg-white/[0.05]">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Report</div>
|
||||
<div className="mt-2 text-lg font-semibold text-white">Report a problem</div>
|
||||
<p className="mt-3 text-sm leading-6 text-slate-300">Use this if a route, role, contributor record, or Group workflow appears broken rather than just unclear.</p>
|
||||
</a>
|
||||
</div>
|
||||
</DocsSection>
|
||||
</div>
|
||||
|
||||
<aside className="hidden xl:block xl:sticky xl:top-24 xl:self-start">
|
||||
<div className="space-y-4 rounded-[28px] border border-white/10 bg-white/[0.03] p-5 shadow-[0_18px_50px_rgba(2,6,23,0.22)]">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/80">Support flow</p>
|
||||
<div className="mt-4 space-y-2">
|
||||
<a href={links.quickstart} className="block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]">Open Quickstart</a>
|
||||
<a href={links.full_documentation} className="block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]">Read full documentation</a>
|
||||
<a href={links.group_studio} className="block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]">Open Group Studio</a>
|
||||
<a href={links.create_group} className="block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]">Create a Group</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[24px] border border-amber-300/20 bg-amber-400/10 p-4 text-amber-50">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-amber-100/80">Quick troubleshooting rule</div>
|
||||
<p className="mt-2 text-sm leading-6 text-amber-50/85">If something feels wrong, check three things first: are you in the right Group context, do you have the right role, and is the content public or internal?</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
561
resources/js/Pages/Group/GroupHelpPage.jsx
Normal file
561
resources/js/Pages/Group/GroupHelpPage.jsx
Normal file
@@ -0,0 +1,561 @@
|
||||
import React from 'react'
|
||||
import { usePage } from '@inertiajs/react'
|
||||
import SeoHead from '../../components/seo/SeoHead'
|
||||
import DocsCallout from '../../components/docs/DocsCallout'
|
||||
import DocsComparisonTable from '../../components/docs/DocsComparisonTable'
|
||||
import DocsFaqAccordion from '../../components/docs/DocsFaqAccordion'
|
||||
import DocsSection from '../../components/docs/DocsSection'
|
||||
import DocsSidebarNav from '../../components/docs/DocsSidebarNav'
|
||||
import DocsStepList from '../../components/docs/DocsStepList'
|
||||
import {
|
||||
BENEFITS,
|
||||
BEST_PRACTICES,
|
||||
COMMON_MISTAKES,
|
||||
CREATE_STEPS,
|
||||
FAQ_ITEMS,
|
||||
FEATURE_CARDS,
|
||||
GOOD_FIT,
|
||||
NOT_YET,
|
||||
ROLE_TABLE,
|
||||
SECTION_ITEMS,
|
||||
STUDIO_AREAS,
|
||||
TROUBLESHOOTING_ITEMS,
|
||||
WORKFLOWS,
|
||||
} from './groupHelpContent'
|
||||
|
||||
function HeroMetric({ label, value, note }) {
|
||||
return (
|
||||
<div className="rounded-[24px] border border-white/10 bg-black/20 p-4">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">{label}</div>
|
||||
<div className="mt-2 text-lg font-semibold text-white">{value}</div>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-400">{note}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TwoColumnChecklist({ title, eyebrow, items, tone = 'sky' }) {
|
||||
const toneClass = tone === 'emerald'
|
||||
? 'border-emerald-300/15 bg-emerald-400/10 text-emerald-100'
|
||||
: 'border-sky-300/15 bg-sky-400/10 text-sky-100'
|
||||
|
||||
return (
|
||||
<div className="rounded-[28px] border border-white/10 bg-black/20 p-5">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">{eyebrow}</p>
|
||||
<h3 className="mt-2 text-xl font-semibold text-white">{title}</h3>
|
||||
<div className="mt-4 space-y-3">
|
||||
{items.map((item) => (
|
||||
<div key={item} className="flex gap-3">
|
||||
<span className={`mt-1 flex h-6 w-6 shrink-0 items-center justify-center rounded-full border ${toneClass}`}>
|
||||
<i className="fa-solid fa-check text-[10px]" />
|
||||
</span>
|
||||
<p className="text-sm leading-6 text-slate-300">{item}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function InfoCard({ title, body, icon }) {
|
||||
return (
|
||||
<div className="rounded-[24px] border border-white/10 bg-black/20 p-5">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.04] text-sky-200">
|
||||
<i className={icon} />
|
||||
</div>
|
||||
<h3 className="mt-4 text-lg font-semibold text-white">{title}</h3>
|
||||
<p className="mt-2 text-sm leading-7 text-slate-300">{body}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function BulletGrid({ items }) {
|
||||
return (
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
{items.map((item) => (
|
||||
<div key={item} className="rounded-[24px] border border-white/10 bg-black/20 p-4 text-sm leading-6 text-slate-300">
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function WorkflowCard({ workflow }) {
|
||||
return (
|
||||
<article className="rounded-[28px] border border-white/10 bg-black/20 p-5">
|
||||
<h3 className="text-xl font-semibold text-white">{workflow.title}</h3>
|
||||
<p className="mt-3 text-sm leading-7 text-slate-300">{workflow.summary}</p>
|
||||
<ul className="mt-4 space-y-2">
|
||||
{workflow.bullets.map((bullet) => (
|
||||
<li key={bullet} className="flex gap-3 text-sm leading-6 text-slate-300">
|
||||
<span className="mt-1.5 h-2 w-2 shrink-0 rounded-full bg-sky-300" />
|
||||
<span>{bullet}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
function TroubleCard({ item }) {
|
||||
return (
|
||||
<article className="rounded-[28px] border border-white/10 bg-black/20 p-5">
|
||||
<h3 className="text-lg font-semibold text-white">{item.title}</h3>
|
||||
<p className="mt-3 text-sm leading-7 text-slate-300">{item.body}</p>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
export default function GroupHelpPage() {
|
||||
const { props } = usePage()
|
||||
const links = props.links || {}
|
||||
const heroJsonLd = [
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
headline: 'Groups Help & Guide',
|
||||
description: props.description,
|
||||
url: props.seo?.canonical,
|
||||
author: {
|
||||
'@type': 'Organization',
|
||||
name: 'Skinbase',
|
||||
},
|
||||
about: ['Groups', 'Collaborative publishing', 'Contributor credit', 'Group Studio', 'Releases'],
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: FAQ_ITEMS.map((item) => ({
|
||||
'@type': 'Question',
|
||||
name: item.question,
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: item.answer,
|
||||
},
|
||||
})),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(14,165,233,0.18),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(250,204,21,0.14),_transparent_22%),linear-gradient(180deg,_#020617_0%,_#030712_100%)] px-4 py-8 sm:px-6 lg:px-8">
|
||||
<SeoHead seo={props.seo || {}} title={props.title} description={props.description} jsonLd={heroJsonLd} />
|
||||
|
||||
<div className="mx-auto max-w-[1500px]">
|
||||
<section id="introduction" className="rounded-[36px] border border-white/10 bg-[linear-gradient(135deg,rgba(15,23,42,0.92),rgba(15,23,42,0.72)),radial-gradient(circle_at_top_right,rgba(125,211,252,0.16),transparent_28%)] p-6 shadow-[0_30px_100px_rgba(2,6,23,0.35)] md:p-8 lg:p-10">
|
||||
<div className="grid gap-8 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80">Groups documentation</p>
|
||||
<h1 className="mt-3 max-w-4xl text-4xl font-semibold tracking-[-0.04em] text-white md:text-5xl xl:text-6xl">Build, manage, and publish through Groups without losing personal credit.</h1>
|
||||
<p className="mt-5 max-w-3xl text-base leading-8 text-slate-300 md:text-lg">Groups on Skinbase Nova are shared creative identities for studios, collectives, release teams, and long-term collaborations. This guide explains when to use them, how to structure roles, how publishing works, and how to keep the public page clear, trustworthy, and easy to maintain.</p>
|
||||
<div className="mt-6 flex flex-wrap gap-3">
|
||||
<a href={links.create_group} className="rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100 transition hover:border-sky-300/40 hover:bg-sky-300/18">Create a Group</a>
|
||||
<a href={links.group_studio} className="rounded-full border border-white/10 bg-white/[0.04] px-5 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.07]">Open Group Studio</a>
|
||||
<a href="#roles-and-permissions" className="rounded-full border border-white/10 bg-black/20 px-5 py-3 text-sm font-semibold text-slate-200 transition hover:border-white/20 hover:bg-white/[0.05]">Jump to roles and permissions</a>
|
||||
</div>
|
||||
{links.quickstart ? (
|
||||
<div className="mt-4">
|
||||
<a href={links.quickstart} className="text-sm font-semibold text-sky-200 underline underline-offset-4">
|
||||
Prefer the shorter onboarding version? Open the Groups Quickstart.
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
{links.faq ? (
|
||||
<div className="mt-2">
|
||||
<a href={links.faq} className="text-sm font-semibold text-slate-300 underline underline-offset-4 hover:text-white">
|
||||
Need faster answers instead? Open the Groups FAQ.
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-3 xl:grid-cols-1">
|
||||
<HeroMetric label="Shared identity" value="One public home for team work" note="Use a Group when a studio, crew, or release team needs its own visible brand." />
|
||||
<HeroMetric label="Preserved credit" value="Authorship stays visible" note="Published by, uploaded by, primary author, and contributors can still reflect the real humans behind the work." />
|
||||
<HeroMetric label="Studio workflow" value="Roles, reviews, projects, releases" note="Groups work best when the team needs structure, not just a different display name." />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="mt-8 grid gap-6 lg:grid-cols-[240px_minmax(0,1fr)] xl:grid-cols-[240px_minmax(0,1fr)_280px]">
|
||||
<DocsSidebarNav sections={SECTION_ITEMS} />
|
||||
|
||||
<div className="space-y-6">
|
||||
<DocsSection
|
||||
id="what-are-groups"
|
||||
eyebrow="Foundations"
|
||||
title="What are Groups?"
|
||||
summary="A Group is a shared creative identity for collaboration and publishing. It is not a replacement for personal profiles. It is the public home for work that belongs to a team, studio, collective, or release-focused collaboration."
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<InfoCard title="Personal profile" icon="fa-solid fa-user" body="Your personal profile is your own portfolio, reputation, and identity. It is where your individual voice, uploads, followers, and personal presence live." />
|
||||
<InfoCard title="Group" icon="fa-solid fa-people-group" body="A Group is the shared layer. It gives a team one public identity for publishing together, managing members, and presenting collaborative work without flattening individual credit." />
|
||||
</div>
|
||||
|
||||
<DocsCallout tone="note" title="The most important rule">
|
||||
A Group is a shared publishing identity, not a way to erase authorship. If real people made the work, their authorship and contribution history should still be represented clearly.
|
||||
</DocsCallout>
|
||||
|
||||
<div className="mt-6 rounded-[28px] border border-white/10 bg-black/20 p-5">
|
||||
<h3 className="text-xl font-semibold text-white">Groups are a good fit for</h3>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{['Design studios', 'Pixel art crews', 'Wallpaper teams', 'Photography collectives', 'Event-based collaborations', 'Release teams'].map((label) => (
|
||||
<span key={label} className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-2 text-sm text-slate-200">{label}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="why-use-a-group"
|
||||
eyebrow="Decision guide"
|
||||
title="Why use a Group?"
|
||||
summary="Use a Group when the work is bigger than one person, or when a shared identity helps the team stay organized, trustworthy, and easy to understand publicly."
|
||||
>
|
||||
<BulletGrid items={BENEFITS} />
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="when-to-create-a-group"
|
||||
eyebrow="Decision guide"
|
||||
title="When should you create a Group?"
|
||||
summary="Create a Group when it solves a real workflow or identity problem. If it is just adding overhead, you probably do not need it yet."
|
||||
>
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<TwoColumnChecklist title="Create one when..." eyebrow="Good fit" items={GOOD_FIT} tone="emerald" />
|
||||
<TwoColumnChecklist title="Hold off when..." eyebrow="Not yet" items={NOT_YET} tone="sky" />
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<DocsCallout tone="tip" title="A simple rule of thumb">
|
||||
If the team needs shared publishing, shared coordination, or a shared public identity more than it needs absolute simplicity, a Group is probably worth it.
|
||||
</DocsCallout>
|
||||
</div>
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="how-groups-work"
|
||||
eyebrow="Model"
|
||||
title="How Groups work"
|
||||
summary="Think of a Group as two connected surfaces: a public identity page and an internal Studio workspace. One is for visibility. The other is for coordination."
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<InfoCard title="Group page" icon="fa-solid fa-earth-americas" body="The public face of the Group with branding, releases, posts, projects, challenges, events, members, and activity." />
|
||||
<InfoCard title="Group Studio" icon="fa-solid fa-sliders" body="The internal workspace for permissions, publishing, review flows, releases, assets, invites, and day-to-day operations." />
|
||||
<InfoCard title="Shared content" icon="fa-solid fa-layer-group" body="Groups can own artworks, collections, posts, projects, challenges, events, assets, and releases depending on the team workflow." />
|
||||
<InfoCard title="Public vs internal" icon="fa-solid fa-lock-open" body="Not everything is public. Some areas are internal, role-based, or review-gated. The public page should be curated. Studio should stay operational." />
|
||||
</div>
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="roles-and-permissions"
|
||||
eyebrow="Team structure"
|
||||
title="Roles and permissions"
|
||||
summary="Keep roles understandable. Most Groups do best when only a small number of people can change settings or manage members, while everyone else gets exactly the access they need and nothing more."
|
||||
>
|
||||
<DocsCallout tone="practice" title="Start simpler than you think">
|
||||
Most new Groups should begin with one Owner, a very small Admin circle, Editors for day-to-day managers, and Contributors for creative participation. Complexity is easier to add later than remove.
|
||||
</DocsCallout>
|
||||
|
||||
<div className="mt-6">
|
||||
<DocsComparisonTable columns={ROLE_TABLE.columns} rows={ROLE_TABLE.rows} caption="Group role comparison" />
|
||||
</div>
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="creating-a-group"
|
||||
eyebrow="Setup"
|
||||
title="Creating a Group"
|
||||
summary="A strong first setup prevents confusion later. Good names, clean branding, and clear role assignments make every other workflow easier."
|
||||
>
|
||||
<DocsStepList items={CREATE_STEPS} />
|
||||
|
||||
<div className="mt-6 grid gap-4 md:grid-cols-2">
|
||||
<DocsCallout tone="practice" title="Setup tips">
|
||||
Choose a name that will still make sense when the Group grows. Add a short description that says what the Group makes, not just what it likes.
|
||||
</DocsCallout>
|
||||
<DocsCallout tone="warning" title="Do not skip ownership decisions">
|
||||
Decide early who should be Owner and who truly needs Admin. Teams create a lot of avoidable friction when this stays vague.
|
||||
</DocsCallout>
|
||||
</div>
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="public-group-page"
|
||||
eyebrow="Public identity"
|
||||
title="Group profile and public page"
|
||||
summary="The public Group page is the identity page for the team. It should feel active, coherent, and curated instead of looking like a random collection of leftovers."
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{['Cover and avatar', 'Description and About', 'Members and leadership', 'Artworks and collections', 'Posts and announcements', 'Projects, challenges, events, releases'].map((item) => (
|
||||
<div key={item} className="rounded-[24px] border border-white/10 bg-black/20 p-4 text-sm font-medium text-slate-200">{item}</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<DocsCallout tone="tip" title="Keep the page feeling alive">
|
||||
Use a consistent visual identity, keep the About copy current, feature the best work, and pin only the update that gives new visitors the best context.
|
||||
</DocsCallout>
|
||||
</div>
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="group-studio"
|
||||
eyebrow="Operations"
|
||||
title="Group Studio"
|
||||
summary="Group Studio is where you switch from being an individual creator to operating inside a shared team context. That context matters every time you publish, review, or manage content."
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<InfoCard title="Personal Studio" icon="fa-solid fa-user-gear" body="Use Personal Studio when you are managing your own portfolio, drafts, uploads, and audience as an individual creator." />
|
||||
<InfoCard title="Group Studio" icon="fa-solid fa-people-roof" body="Use Group Studio when the work belongs to the shared identity, or when roles, reviews, projects, releases, and member access need to be respected." />
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<BulletGrid items={STUDIO_AREAS} />
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<DocsCallout tone="warning" title="Check context before publishing">
|
||||
The easiest way to create confusing attribution is to publish from the wrong context. If the work belongs to the Group, confirm that Group Studio is active before you submit or publish.
|
||||
</DocsCallout>
|
||||
</div>
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="publishing-as-a-group"
|
||||
eyebrow="Publishing"
|
||||
title="Publishing as a Group"
|
||||
summary="Publishing as a Group means the shared identity is the public publish surface. It does not mean the Group replaces every human role in the record."
|
||||
>
|
||||
<div className="rounded-[28px] border border-white/10 bg-black/20 p-5">
|
||||
<h3 className="text-xl font-semibold text-white">How to read the publishing record</h3>
|
||||
<div className="mt-4 grid gap-3 md:grid-cols-2">
|
||||
<div className="rounded-[22px] border border-white/10 bg-white/[0.03] p-4">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Published by</div>
|
||||
<div className="mt-2 text-base font-semibold text-white">Warlock</div>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-300">The shared identity the work appears under publicly.</p>
|
||||
</div>
|
||||
<div className="rounded-[22px] border border-white/10 bg-white/[0.03] p-4">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Uploaded by</div>
|
||||
<div className="mt-2 text-base font-semibold text-white">Gregor</div>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-300">The person who performed the upload or publishing action.</p>
|
||||
</div>
|
||||
<div className="rounded-[22px] border border-white/10 bg-white/[0.03] p-4">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Primary author</div>
|
||||
<div className="mt-2 text-base font-semibold text-white">Gregor</div>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-300">The person who should be understood as the main author of the work.</p>
|
||||
</div>
|
||||
<div className="rounded-[22px] border border-white/10 bg-white/[0.03] p-4">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Contributors</div>
|
||||
<div className="mt-2 text-base font-semibold text-white">Denis, Paula</div>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-300">Additional people who made meaningful creative contributions.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid gap-4 md:grid-cols-2">
|
||||
<DocsCallout tone="note" title="Why this matters">
|
||||
Group publishing creates a shared public identity for the work, but personal authorship, accountability, and contribution history should still be easy to understand.
|
||||
</DocsCallout>
|
||||
<DocsCallout tone="warning" title="Do not use Group publishing to hide authorship">
|
||||
If the work is mainly one person\'s piece, make sure the primary author and contributors reflect that reality clearly.
|
||||
</DocsCallout>
|
||||
</div>
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="contributor-credit"
|
||||
eyebrow="Attribution"
|
||||
title="Contributor credit and authorship"
|
||||
summary="Correct attribution keeps the Group healthy. It builds trust inside the team, makes the public record clearer, and reduces avoidable disputes later."
|
||||
>
|
||||
<BulletGrid items={[
|
||||
'Always credit real contributors, even when the Group brand is stronger than any single member.',
|
||||
'Use role labels when they add clarity, such as packaging lead, curator, reviewer, or art director.',
|
||||
'Do not swap uploader and author just because one person clicked Publish.',
|
||||
'Discuss credits early for bigger releases so nobody is negotiating attribution after launch day.',
|
||||
]} />
|
||||
|
||||
<div className="mt-6">
|
||||
<DocsCallout tone="warning" title="Incorrect credit causes real friction">
|
||||
Attribution problems are rarely just metadata problems. They affect trust, morale, and how future collaborators feel about the Group.
|
||||
</DocsCallout>
|
||||
</div>
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="member-management"
|
||||
eyebrow="Team health"
|
||||
title="Inviting members and managing the team"
|
||||
summary="Healthy Groups are clear about who has access, why they have it, and how that access changes as the team grows."
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<InfoCard title="Invites and onboarding" icon="fa-solid fa-user-plus" body="Invite people with the smallest role that still lets them do the work. Explain expectations before they accept so there is no ambiguity about ownership or workflow." />
|
||||
<InfoCard title="Role reviews" icon="fa-solid fa-user-check" body="Review roles periodically. People change, projects end, and old permissions should not stay permanent by accident." />
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid gap-4 md:grid-cols-2">
|
||||
<DocsCallout tone="practice" title="Role assignment guidance">
|
||||
Keep Owner count very limited, give Admin only to trusted operators, use Editor for content managers, and keep Contributor focused on creation.
|
||||
</DocsCallout>
|
||||
<DocsCallout tone="note" title="Join requests and recruiting">
|
||||
If your Group supports join requests or recruiting, use them with a real onboarding process. Recruiting without follow-through makes the Group feel abandoned.
|
||||
</DocsCallout>
|
||||
</div>
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="review-workflow"
|
||||
eyebrow="Quality control"
|
||||
title="Review queue and approval workflow"
|
||||
summary="Review flows help larger or more structured Groups keep public quality high without forcing every trusted team to work the same way."
|
||||
>
|
||||
<DocsStepList
|
||||
items={[
|
||||
{ title: 'Contributor submits a draft', description: 'The work enters the Group pipeline without immediately going public.' },
|
||||
{ title: 'Reviewer checks the work', description: 'Editors, admins, or designated reviewers confirm quality, context, and credit.' },
|
||||
{ title: 'Approve, request changes, or reject', description: 'Feedback should be specific enough that the creator knows what to do next.' },
|
||||
{ title: 'Publish when ready', description: 'Once the draft is approved, the right person can publish it under the correct Group context.' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="mt-6 grid gap-4 md:grid-cols-2">
|
||||
<DocsCallout tone="tip" title="When direct publishing makes sense">
|
||||
Small, trusted teams often move faster with direct publishing. Use review only when it protects quality or reduces confusion.
|
||||
</DocsCallout>
|
||||
<DocsCallout tone="practice" title="When review-first helps">
|
||||
Larger teams, new contributors, and release-heavy groups usually benefit from a review queue because it catches context, permission, and attribution mistakes before they go public.
|
||||
</DocsCallout>
|
||||
</div>
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="group-features"
|
||||
eyebrow="Feature ecosystem"
|
||||
title="Posts, projects, challenges, events, assets, and releases"
|
||||
summary="These features are most useful when they connect. A healthy Group does not use them all at once. It chooses the smallest set that makes the public story and internal workflow clearer."
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{FEATURE_CARDS.map((card, index) => (
|
||||
<InfoCard key={card.title} title={card.title} body={card.body} icon={['fa-solid fa-diagram-project', 'fa-solid fa-bullseye', 'fa-solid fa-calendar-day', 'fa-solid fa-box-open', 'fa-solid fa-rocket', 'fa-solid fa-bullhorn'][index]} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<DocsCallout tone="note" title="A practical progression">
|
||||
Many teams start with artworks and posts, then add projects when collaboration gets busier, and use releases when the Group is ready for stronger public storytelling around major drops.
|
||||
</DocsCallout>
|
||||
</div>
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="tips-and-best-practices"
|
||||
eyebrow="Operating well"
|
||||
title="Tips and best practices"
|
||||
summary="Most support questions come from a small set of preventable mistakes. These habits keep Groups easier to manage and easier to trust."
|
||||
>
|
||||
<BulletGrid items={BEST_PRACTICES} />
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="common-mistakes"
|
||||
eyebrow="Avoid these"
|
||||
title="Common mistakes to avoid"
|
||||
summary="Groups become confusing when identity, permissions, and attribution drift out of sync."
|
||||
>
|
||||
<DocsCallout tone="warning" title="The fastest way to make a Group feel unreliable">
|
||||
Mix unclear roles with vague attribution and inconsistent publishing context. Users will stop trusting what they are looking at.
|
||||
</DocsCallout>
|
||||
|
||||
<div className="mt-6 grid gap-3 md:grid-cols-2">
|
||||
{COMMON_MISTAKES.map((item) => (
|
||||
<div key={item} className="rounded-[24px] border border-white/10 bg-black/20 p-4 text-sm leading-6 text-slate-300">{item}</div>
|
||||
))}
|
||||
</div>
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="suggested-workflows"
|
||||
eyebrow="Patterns"
|
||||
title="Suggested workflows"
|
||||
summary="You do not need one perfect workflow. You need the right amount of structure for the team you actually have."
|
||||
>
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
{WORKFLOWS.map((workflow) => <WorkflowCard key={workflow.title} workflow={workflow} />)}
|
||||
</div>
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="faq"
|
||||
eyebrow="FAQ"
|
||||
title="Frequently asked questions"
|
||||
summary="Short answers to the questions people most often ask before creating, joining, or managing a Group."
|
||||
>
|
||||
<DocsFaqAccordion items={FAQ_ITEMS} />
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="troubleshooting"
|
||||
eyebrow="Troubleshooting"
|
||||
title="Common problems and how to think through them"
|
||||
summary="If something feels confusing, start with context, role, and visibility. Most Group issues live in one of those three buckets."
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{TROUBLESHOOTING_ITEMS.map((item) => <TroubleCard key={item.title} item={item} />)}
|
||||
</div>
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="need-help"
|
||||
eyebrow="Support"
|
||||
title="Still need help?"
|
||||
summary="Use these next steps if you are ready to create a Group, need to check your current setup, or want to contact Skinbase support."
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<a href={links.create_group} className="rounded-[28px] border border-sky-300/20 bg-sky-300/10 p-5 transition hover:border-sky-300/35 hover:bg-sky-300/15">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-100/80">Create</div>
|
||||
<div className="mt-2 text-lg font-semibold text-white">Create your first Group</div>
|
||||
<p className="mt-3 text-sm leading-6 text-sky-50/80">Start with branding, visibility, and your first member invites.</p>
|
||||
</a>
|
||||
<a href={links.group_studio} className="rounded-[28px] border border-white/10 bg-black/20 p-5 transition hover:border-white/20 hover:bg-white/[0.05]">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Manage</div>
|
||||
<div className="mt-2 text-lg font-semibold text-white">Open Group Studio</div>
|
||||
<p className="mt-3 text-sm leading-6 text-slate-300">Check members, workflow, releases, recruitment, and review status.</p>
|
||||
</a>
|
||||
<a href={links.contact_support} className="rounded-[28px] border border-white/10 bg-black/20 p-5 transition hover:border-white/20 hover:bg-white/[0.05]">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Contact</div>
|
||||
<div className="mt-2 text-lg font-semibold text-white">Contact support</div>
|
||||
<p className="mt-3 text-sm leading-6 text-slate-300">Use the general support flow if you need help untangling an account or workflow issue.</p>
|
||||
</a>
|
||||
<a href={links.report_issue} className="rounded-[28px] border border-white/10 bg-black/20 p-5 transition hover:border-white/20 hover:bg-white/[0.05]">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Report</div>
|
||||
<div className="mt-2 text-lg font-semibold text-white">Report a problem</div>
|
||||
<p className="mt-3 text-sm leading-6 text-slate-300">Use this if a route, permission, credit record, or workflow appears broken.</p>
|
||||
</a>
|
||||
</div>
|
||||
</DocsSection>
|
||||
</div>
|
||||
|
||||
<aside className="hidden xl:block xl:sticky xl:top-24 xl:self-start">
|
||||
<div className="space-y-4 rounded-[28px] border border-white/10 bg-white/[0.03] p-5 shadow-[0_18px_50px_rgba(2,6,23,0.22)]">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/80">Quick actions</p>
|
||||
<div className="mt-4 space-y-2">
|
||||
<a href={links.groups_directory} className="block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]">Browse public Groups</a>
|
||||
<a href={links.group_studio} className="block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]">Open Group Studio</a>
|
||||
{links.faq ? <a href={links.faq} className="block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]">Open Groups FAQ</a> : null}
|
||||
<a href="#publishing-as-a-group" className="block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]">Review publishing guidance</a>
|
||||
<a href="#contributor-credit" className="block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]">Check contributor credit rules</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[24px] border border-amber-300/20 bg-amber-400/10 p-4 text-amber-50">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-amber-100/80">Read this before launch day</div>
|
||||
<p className="mt-2 text-sm leading-6 text-amber-50/85">Before the first public release or artwork, confirm the Group context, contributor credit, and review expectations. Those three checks prevent most avoidable confusion.</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
79
resources/js/Pages/Group/GroupIndex.jsx
Normal file
79
resources/js/Pages/Group/GroupIndex.jsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import React from 'react'
|
||||
import { usePage } from '@inertiajs/react'
|
||||
import SeoHead from '../../components/seo/SeoHead'
|
||||
import GroupPromoCard from '../../components/groups/GroupPromoCard'
|
||||
import GroupTrendingSection from '../../components/groups/GroupTrendingSection'
|
||||
import GroupBrowseFilters from '../../components/groups/GroupBrowseFilters'
|
||||
import GroupDiscoveryCard from '../../components/groups/GroupDiscoveryCard'
|
||||
import GroupLeaderboardCard from '../../components/groups/GroupLeaderboardCard'
|
||||
|
||||
export default function GroupIndex() {
|
||||
const { props } = usePage()
|
||||
const groups = props.groups?.data || []
|
||||
const surfaces = Array.isArray(props.surfaces) ? props.surfaces : []
|
||||
const currentSurface = props.currentSurface || 'featured'
|
||||
const highlightSections = Array.isArray(props.highlightSections) ? props.highlightSections : []
|
||||
const leaderboardItems = Array.isArray(props.leaderboard?.items) ? props.leaderboard.items : []
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.16),_transparent_28%),linear-gradient(180deg,_#020617_0%,_#02040a_100%)] px-4 py-10 sm:px-6 lg:px-8">
|
||||
<SeoHead title="Groups - Skinbase" description={props.description} />
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<section className="rounded-[32px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80">Groups</p>
|
||||
<h1 className="mt-2 text-4xl font-semibold text-white">Collective publishing identities</h1>
|
||||
<p className="mt-4 max-w-3xl text-sm leading-6 text-slate-300">Discover collaborative studios, follow shared creative brands, and browse the artworks, releases, and collections published under each group identity.</p>
|
||||
<GroupBrowseFilters surfaces={surfaces} currentSurface={currentSurface} />
|
||||
</section>
|
||||
|
||||
<div className="mt-8">
|
||||
<GroupPromoCard
|
||||
group={props.spotlightGroup}
|
||||
eyebrow="Public groups"
|
||||
title="Find collaborative identities with real momentum"
|
||||
description="Groups now sit alongside creators and artworks across Nova, making shared publishing, team recruitment, and release-driven collaboration easier to discover."
|
||||
ctaLabel="Open spotlight"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{highlightSections.map((section) => (
|
||||
<GroupTrendingSection
|
||||
key={section.key}
|
||||
title={section.title}
|
||||
description={section.description}
|
||||
items={section.items || []}
|
||||
href={`/groups?surface=${encodeURIComponent(section.key)}`}
|
||||
/>
|
||||
))}
|
||||
|
||||
{leaderboardItems.length > 0 ? (
|
||||
<section className="mt-10">
|
||||
<div className="mb-5 flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-[-0.02em] text-white">Monthly group leaderboard</h2>
|
||||
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-400">A fast view of the collaborative teams moving the most attention and publishing energy right now.</p>
|
||||
</div>
|
||||
<a href="/leaderboard?type=groups&period=monthly" className="text-sm font-semibold text-sky-200 transition hover:text-white">View leaderboard</a>
|
||||
</div>
|
||||
<div className="grid gap-4 xl:grid-cols-3">
|
||||
{leaderboardItems.slice(0, 3).map((item) => <GroupLeaderboardCard key={item.entity?.id || item.rank} item={item} />)}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="mt-10">
|
||||
<div className="mb-5 flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-[-0.02em] text-white">Browse groups</h2>
|
||||
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-400">Filter the directory by discovery surface, then jump into each group’s public page for artworks, releases, projects, events, and activity.</p>
|
||||
</div>
|
||||
<div className="text-sm text-slate-500">{Number(props.groups?.meta?.total || 0).toLocaleString()} public groups</div>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{groups.map((group) => <GroupDiscoveryCard key={group.slug || group.id} group={group} />)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
70
resources/js/Pages/Group/GroupPostShow.jsx
Normal file
70
resources/js/Pages/Group/GroupPostShow.jsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React from 'react'
|
||||
import { usePage } from '@inertiajs/react'
|
||||
import SeoHead from '../../components/seo/SeoHead'
|
||||
|
||||
function csrfToken() {
|
||||
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||||
}
|
||||
|
||||
export default function GroupPostShow() {
|
||||
const { props } = usePage()
|
||||
const group = props.group || {}
|
||||
const post = props.post || {}
|
||||
const recentPosts = Array.isArray(props.recentPosts) ? props.recentPosts : []
|
||||
|
||||
const submitReport = async () => {
|
||||
if (!props.reportEndpoint || !post.id) return
|
||||
|
||||
const reason = window.prompt('Reason for reporting this post?')
|
||||
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_post', target_id: post.id, reason }),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.16),_transparent_28%),linear-gradient(180deg,_#020617_0%,_#02040a_100%)] px-4 py-10 sm:px-6 lg:px-8">
|
||||
<SeoHead seo={props.seo || {}} title={`${post.title || group.name} - Skinbase`} description={post.excerpt || group.headline || group.bio || 'Group post'} />
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<article className="rounded-[32px] border border-white/10 bg-white/[0.03] p-6 sm:p-8">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<a href={group.urls?.public} className="text-sm font-semibold text-sky-200">← Back to {group.name}</a>
|
||||
{props.reportEndpoint ? <button type="button" onClick={submitReport} className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-2 text-sm font-semibold text-white">Report</button> : null}
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-2 text-xs uppercase tracking-[0.16em] text-slate-400">
|
||||
{post.type ? <span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1">{post.type}</span> : null}
|
||||
{post.is_pinned ? <span className="rounded-full border border-amber-300/20 bg-amber-400/10 px-3 py-1 text-amber-100">Pinned</span> : null}
|
||||
</div>
|
||||
<h1 className="mt-5 text-4xl font-semibold text-white">{post.title}</h1>
|
||||
<div className="mt-3 text-sm text-slate-400">{post.author?.name || post.author?.username || group.name} • {post.published_at ? new Date(post.published_at).toLocaleString() : 'Recently'}</div>
|
||||
{post.excerpt ? <p className="mt-6 text-lg leading-8 text-slate-200">{post.excerpt}</p> : null}
|
||||
<div className="mt-8 whitespace-pre-wrap text-sm leading-7 text-slate-300">{post.content || ''}</div>
|
||||
</article>
|
||||
|
||||
{recentPosts.length > 0 ? (
|
||||
<section className="mt-8 rounded-[32px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-2xl font-semibold text-white">More from {group.name}</h2>
|
||||
<div className="mt-4 grid gap-4 md:grid-cols-2">
|
||||
{recentPosts.filter((item) => item.id !== post.id).map((item) => (
|
||||
<a key={item.id} href={item.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">{item.type}</div>
|
||||
<div className="mt-2 text-lg font-semibold text-white">{item.title}</div>
|
||||
<p className="mt-2 text-sm text-slate-400">{item.excerpt || 'Read the full post.'}</p>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
105
resources/js/Pages/Group/GroupProjectShow.jsx
Normal file
105
resources/js/Pages/Group/GroupProjectShow.jsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React from 'react'
|
||||
import { usePage } from '@inertiajs/react'
|
||||
import SeoHead from '../../components/seo/SeoHead'
|
||||
|
||||
function ArtworkGrid({ artworks }) {
|
||||
if (!Array.isArray(artworks) || artworks.length === 0) {
|
||||
return <p className="mt-4 text-sm text-slate-400">No linked artworks yet.</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-4 grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{artworks.map((artwork) => (
|
||||
<a key={artwork.id} href={artwork.url} className="overflow-hidden rounded-[24px] border border-white/10 bg-black/20">
|
||||
{artwork.thumb ? <img src={artwork.thumb} alt={artwork.title} className="aspect-[4/3] w-full object-cover" /> : null}
|
||||
<div className="p-4">
|
||||
<h3 className="text-base font-semibold text-white">{artwork.title}</h3>
|
||||
<p className="mt-1 text-sm text-slate-400">{artwork.author || 'Artwork'}</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function GroupProjectShow() {
|
||||
const { props } = usePage()
|
||||
const group = props.group || {}
|
||||
const project = props.project || {}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.16),_transparent_28%),linear-gradient(180deg,_#020617_0%,_#02040a_100%)] px-4 py-10 sm:px-6 lg:px-8">
|
||||
<SeoHead seo={props.seo || {}} title={`${project.title || group.name} - Skinbase`} description={project.summary || project.description || group.headline || 'Group project'} />
|
||||
<div className="mx-auto max-w-6xl space-y-8">
|
||||
<section className="overflow-hidden rounded-[32px] border border-white/10 bg-white/[0.03]">
|
||||
{project.cover_url ? <img src={project.cover_url} alt={project.title} className="h-56 w-full object-cover" /> : <div className="h-40 bg-white/[0.03]" />}
|
||||
<div className="p-6">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<a href={group.urls?.public} className="text-sm font-semibold text-sky-200">{group.name}</a>
|
||||
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-300">{project.status}</span>
|
||||
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-300">{project.visibility}</span>
|
||||
</div>
|
||||
<h1 className="mt-4 text-4xl font-semibold text-white">{project.title}</h1>
|
||||
{project.summary ? <p className="mt-4 max-w-3xl text-sm leading-7 text-slate-300">{project.summary}</p> : null}
|
||||
<div className="mt-5 flex flex-wrap gap-4 text-xs text-slate-400">
|
||||
{project.start_date ? <span>Started {new Date(project.start_date).toLocaleDateString()}</span> : null}
|
||||
{project.target_date ? <span>Target {new Date(project.target_date).toLocaleDateString()}</span> : null}
|
||||
{project.released_at ? <span>Released {new Date(project.released_at).toLocaleDateString()}</span> : null}
|
||||
{project.lead?.name || project.lead?.username ? <span>Lead: {project.lead?.name || project.lead?.username}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="grid gap-8 xl:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
|
||||
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-2xl font-semibold text-white">Overview</h2>
|
||||
<p className="mt-4 text-sm leading-7 text-slate-300">{project.description || 'No long-form description yet.'}</p>
|
||||
{Array.isArray(project.milestones) && project.milestones.length > 0 ? <div className="mt-6 space-y-3">{project.milestones.map((milestone) => <div key={milestone.id} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-4"><div className="flex items-center justify-between gap-3"><div className="font-semibold text-white">{milestone.title}</div><span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-300">{milestone.status}</span></div>{milestone.summary ? <p className="mt-2 text-sm text-slate-400">{milestone.summary}</p> : null}{milestone.owner?.name || milestone.owner?.username ? <div className="mt-2 text-xs text-slate-500">Owner: {milestone.owner?.name || milestone.owner?.username}</div> : null}</div>)}</div> : null}
|
||||
<ArtworkGrid artworks={project.artworks} />
|
||||
</section>
|
||||
|
||||
<div className="space-y-8">
|
||||
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-2xl font-semibold text-white">Pipeline</h2>
|
||||
<div className="mt-4 text-sm leading-7 text-slate-300">This project currently has {project.counts?.milestones || 0} milestones and is linked to {project.release_count || project.counts?.releases || 0} releases.</div>
|
||||
</section>
|
||||
{Array.isArray(project.assets) && project.assets.length > 0 ? (
|
||||
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-2xl font-semibold text-white">Assets</h2>
|
||||
<div className="mt-4 space-y-3">
|
||||
{project.assets.map((asset) => (
|
||||
<a key={asset.id} href={asset.download_url} className="block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white">
|
||||
<div className="font-semibold">{asset.title}</div>
|
||||
<div className="mt-1 text-xs uppercase tracking-[0.16em] text-slate-400">{asset.category} • {asset.visibility}</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{Array.isArray(project.team) && project.team.length > 0 ? (
|
||||
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-2xl font-semibold text-white">Team</h2>
|
||||
<div className="mt-4 space-y-3">
|
||||
{project.team.map((member) => (
|
||||
<div key={member.id} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white">
|
||||
<div className="font-semibold">{member.name || member.username}</div>
|
||||
<div className="mt-1 text-xs uppercase tracking-[0.16em] text-slate-400">{member.role_label || (member.is_lead ? 'Lead' : 'Contributor')}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{project.pinned_post ? (
|
||||
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-2xl font-semibold text-white">Pinned update</h2>
|
||||
<a href={project.pinned_post.url} className="mt-4 inline-block text-sm font-semibold text-sky-200">{project.pinned_post.title}</a>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
322
resources/js/Pages/Group/GroupQuickstartPage.jsx
Normal file
322
resources/js/Pages/Group/GroupQuickstartPage.jsx
Normal file
@@ -0,0 +1,322 @@
|
||||
import React from 'react'
|
||||
import { usePage } from '@inertiajs/react'
|
||||
import DocsCallout from '../../components/docs/DocsCallout'
|
||||
import DocsSection from '../../components/docs/DocsSection'
|
||||
import DocsSidebarNav from '../../components/docs/DocsSidebarNav'
|
||||
import DocsStepList from '../../components/docs/DocsStepList'
|
||||
import QuickstartChecklist from '../../components/docs/QuickstartChecklist'
|
||||
import QuickstartNextSteps from '../../components/docs/QuickstartNextSteps'
|
||||
import SeoHead from '../../components/seo/SeoHead'
|
||||
import {
|
||||
COMPARISON_CARDS,
|
||||
COMMON_MISTAKES,
|
||||
CREATE_STEPS,
|
||||
CREDIT_TERMS,
|
||||
FIRST_WEEK_BEST_PRACTICES,
|
||||
GOOD_FIT,
|
||||
NEXT_STEPS,
|
||||
NOT_NEEDED_YET,
|
||||
PUBLISH_STEPS,
|
||||
QUICK_CHECKLIST,
|
||||
ROLE_CARDS,
|
||||
SECTION_ITEMS,
|
||||
SETUP_TASKS,
|
||||
} from './groupQuickstartContent'
|
||||
|
||||
function HeroStat({ label, value, note }) {
|
||||
return (
|
||||
<div className="rounded-[22px] border border-white/10 bg-black/20 p-4">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">{label}</div>
|
||||
<div className="mt-2 text-lg font-semibold text-white">{value}</div>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-400">{note}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ComparisonCard({ card }) {
|
||||
return (
|
||||
<article className="rounded-[28px] border border-white/10 bg-black/20 p-5">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.04] text-sky-200">
|
||||
<i className={card.icon} />
|
||||
</div>
|
||||
<h3 className="mt-4 text-xl font-semibold text-white">{card.title}</h3>
|
||||
<ul className="mt-4 space-y-2">
|
||||
{card.bullets.map((item) => (
|
||||
<li key={item} className="flex gap-3 text-sm leading-6 text-slate-300">
|
||||
<span className="mt-1.5 h-2 w-2 shrink-0 rounded-full bg-sky-300" />
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
function SimpleListCard({ title, eyebrow, items, tone = 'sky' }) {
|
||||
const toneClass = tone === 'emerald'
|
||||
? 'border-emerald-300/15 bg-emerald-400/10 text-emerald-50'
|
||||
: 'border-white/10 bg-black/20 text-white'
|
||||
|
||||
return (
|
||||
<div className={`rounded-[28px] border p-5 ${toneClass}`}>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] opacity-80">{eyebrow}</p>
|
||||
<h3 className="mt-2 text-xl font-semibold">{title}</h3>
|
||||
<ul className="mt-4 space-y-3">
|
||||
{items.map((item) => (
|
||||
<li key={item} className="flex gap-3 text-sm leading-6 opacity-95">
|
||||
<span className="mt-1 flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-white/10 bg-white/[0.06] text-[10px]">
|
||||
<i className="fa-solid fa-check" />
|
||||
</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RoleCard({ role }) {
|
||||
return (
|
||||
<article className="rounded-[26px] border border-white/10 bg-black/20 p-5">
|
||||
<div className="inline-flex rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-sky-100">
|
||||
{role.role}
|
||||
</div>
|
||||
<p className="mt-4 text-sm leading-7 text-slate-300">{role.summary}</p>
|
||||
<p className="mt-3 text-sm font-medium text-slate-200">{role.note}</p>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
function CompactGrid({ items }) {
|
||||
return (
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
{items.map((item) => (
|
||||
<div key={item} className="rounded-[22px] border border-white/10 bg-black/20 p-4 text-sm leading-6 text-slate-300">
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CreditCard({ item }) {
|
||||
return (
|
||||
<div className="rounded-[24px] border border-white/10 bg-black/20 p-4">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">{item.label}</div>
|
||||
<div className="mt-2 text-base font-semibold text-white">{item.value}</div>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-300">{item.note}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function GroupQuickstartPage() {
|
||||
const { props } = usePage()
|
||||
const links = props.links || {}
|
||||
const nextSteps = NEXT_STEPS.map((item) => ({
|
||||
...item,
|
||||
href: links[item.linkKey] || '#',
|
||||
}))
|
||||
|
||||
const jsonLd = [
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
headline: props.title,
|
||||
description: props.description,
|
||||
url: props.seo?.canonical,
|
||||
author: {
|
||||
'@type': 'Organization',
|
||||
name: 'Skinbase',
|
||||
},
|
||||
about: ['Groups', 'Quickstart', 'Collaborative publishing', 'Contributor credit'],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(14,165,233,0.18),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(250,204,21,0.14),_transparent_22%),linear-gradient(180deg,_#020617_0%,_#030712_100%)] px-4 py-8 sm:px-6 lg:px-8">
|
||||
<SeoHead seo={props.seo || {}} title={props.title} description={props.description} jsonLd={jsonLd} />
|
||||
|
||||
<div className="mx-auto max-w-[1380px]">
|
||||
<section id="introduction" className="rounded-[36px] border border-white/10 bg-[linear-gradient(135deg,rgba(15,23,42,0.92),rgba(15,23,42,0.74)),radial-gradient(circle_at_top_right,rgba(125,211,252,0.18),transparent_28%)] p-6 shadow-[0_30px_100px_rgba(2,6,23,0.35)] md:p-8 lg:p-10">
|
||||
<div className="grid gap-8 xl:grid-cols-[minmax(0,1fr)_340px]">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80">Groups quickstart</p>
|
||||
<h1 className="mt-3 max-w-4xl text-4xl font-semibold tracking-[-0.04em] text-white md:text-5xl xl:text-6xl">Get started with Groups fast and publish together without losing individual credit.</h1>
|
||||
<p className="mt-5 max-w-3xl text-base leading-8 text-slate-300 md:text-lg">This quickstart is the fast path from curiosity to first success. It shows what a Group is, when to use one, how to invite the right people, and how to publish your first Group artwork with contributor credit handled properly.</p>
|
||||
<div className="mt-6 flex flex-wrap gap-3">
|
||||
<a href={links.create_group} className="rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100 transition hover:border-sky-300/40 hover:bg-sky-300/18">Create a Group</a>
|
||||
<a href={links.group_studio} className="rounded-full border border-white/10 bg-white/[0.04] px-5 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.07]">Open Group Studio</a>
|
||||
<a href={links.full_documentation} className="rounded-full border border-white/10 bg-black/20 px-5 py-3 text-sm font-semibold text-slate-200 transition hover:border-white/20 hover:bg-white/[0.05]">Read full Groups documentation</a>
|
||||
</div>
|
||||
{links.faq ? (
|
||||
<div className="mt-4">
|
||||
<a href={links.faq} className="text-sm font-semibold text-sky-200 underline underline-offset-4">
|
||||
Need quick answers instead of the full guide? Open the Groups FAQ.
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-3 xl:grid-cols-1">
|
||||
<HeroStat label="What a Group is" value="A shared team identity" note="Use it when a studio, crew, or project needs one public home instead of scattered personal uploads." />
|
||||
<HeroStat label="What stays visible" value="Real contributor credit" note="Published by, uploaded by, primary author, and contributor roles can still reflect the real people behind the work." />
|
||||
<HeroStat label="First win" value="Create, invite, publish" note="The goal of this page is simple: get you to a clean first Group publish without confusion." />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="mt-8 grid gap-6 lg:grid-cols-[220px_minmax(0,1fr)]">
|
||||
<DocsSidebarNav sections={SECTION_ITEMS} />
|
||||
|
||||
<div className="space-y-6">
|
||||
<DocsSection
|
||||
id="what-is-a-group"
|
||||
eyebrow="Start here"
|
||||
title="What is a Group?"
|
||||
summary="A Group is a shared creative identity for multiple people. It lets a team publish together under one name while still showing who uploaded, authored, and contributed to the work."
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{COMPARISON_CARDS.map((card) => <ComparisonCard key={card.title} card={card} />)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<DocsCallout tone="note" title="The key idea to keep in your head">
|
||||
Group and personal publishing can coexist. A Group gives the team a shared identity, but it should not erase the people behind the work.
|
||||
</DocsCallout>
|
||||
</div>
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="when-to-use"
|
||||
eyebrow="Decision"
|
||||
title="When should you use a Group?"
|
||||
summary="Use a Group when collaboration is real enough to need shared identity, shared workflow, or shared publishing. Skip it for now if it only adds overhead."
|
||||
>
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<SimpleListCard title="Use a Group when..." eyebrow="Good fit" items={GOOD_FIT} tone="emerald" />
|
||||
<SimpleListCard title="You can wait when..." eyebrow="Not necessary yet" items={NOT_NEEDED_YET} />
|
||||
</div>
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="create-first-group"
|
||||
eyebrow="Build the foundation"
|
||||
title="Create your first Group"
|
||||
summary="The fastest clean start is a simple start. Get the identity created first, then improve it as the Group becomes active."
|
||||
>
|
||||
<DocsStepList items={CREATE_STEPS} />
|
||||
|
||||
<div className="mt-6">
|
||||
<DocsCallout tone="tip" title="Start simple">
|
||||
You do not need perfect branding or a complex team structure on day one. You need a clear name, a usable page, and the right first members.
|
||||
</DocsCallout>
|
||||
</div>
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="setup-properly"
|
||||
eyebrow="Right after creation"
|
||||
title="Set up your Group properly"
|
||||
summary="The first few setup moves decide whether the Group feels trustworthy and active or unfinished and confusing."
|
||||
>
|
||||
<CompactGrid items={SETUP_TASKS} />
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="invite-and-roles"
|
||||
eyebrow="Team setup"
|
||||
title="Invite members and choose roles"
|
||||
summary="Keep the role model clear. Most teams should stay simple at first: very few Owners, very few Admins, Editors for trusted content operators, and Contributors for most collaborators."
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
{ROLE_CARDS.map((role) => <RoleCard key={role.role} role={role} />)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid gap-4 md:grid-cols-2">
|
||||
<DocsCallout tone="practice" title="Recommended first move">
|
||||
Invite your first members, assign only the roles they need right now, and avoid advanced permission tuning until the team has real workflow pressure.
|
||||
</DocsCallout>
|
||||
<DocsCallout tone="note" title="Advanced overrides can wait">
|
||||
If you need permission overrides later, you can add them later. The quickstart path is deliberately simpler than the full feature set.
|
||||
</DocsCallout>
|
||||
</div>
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="publish-first-artwork"
|
||||
eyebrow="First success"
|
||||
title="Publish your first artwork as a Group"
|
||||
summary="This is where new teams get tripped up most often. The artwork should appear under the Group publicly, but the people behind it should still be represented correctly."
|
||||
>
|
||||
<DocsStepList items={PUBLISH_STEPS} />
|
||||
|
||||
<div className="mt-6">
|
||||
<DocsCallout tone="warning" title="Always check publishing context before the final click">
|
||||
Confirm whether you are publishing as your personal profile or as the Group. That one check prevents a lot of cleanup later.
|
||||
</DocsCallout>
|
||||
</div>
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="contributor-credit"
|
||||
eyebrow="Credit"
|
||||
title="Understand contributor credit"
|
||||
summary="Groups are for shared identity, not for hiding who did the actual work. Before the first public publish, make sure the credit record reflects reality."
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
{CREDIT_TERMS.map((item) => <CreditCard key={item.label} item={item} />)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<DocsCallout tone="practice" title="Best practice">
|
||||
Review contributor credit before every first release, first Group artwork, or first major collaborative drop. Do not leave attribution as an afterthought.
|
||||
</DocsCallout>
|
||||
</div>
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="first-week-best-practices"
|
||||
eyebrow="First week"
|
||||
title="First-week best practices"
|
||||
summary="The first week should make the Group feel intentional, active, and easy to understand."
|
||||
>
|
||||
<CompactGrid items={FIRST_WEEK_BEST_PRACTICES} />
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection
|
||||
id="common-mistakes"
|
||||
eyebrow="Avoid these"
|
||||
title="Common mistakes to avoid"
|
||||
summary="These are the fastest ways to make a new Group feel confusing or unreliable."
|
||||
>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
{COMMON_MISTAKES.map((item) => (
|
||||
<div key={item} className="rounded-[24px] border border-amber-300/15 bg-amber-400/10 p-4 text-sm leading-6 text-amber-50/95">
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</DocsSection>
|
||||
|
||||
<section id="quick-checklist" className="scroll-mt-24">
|
||||
<QuickstartChecklist
|
||||
title="Use this before your first Group publish"
|
||||
summary="This is the lightweight completion list you want to be able to say yes to before the Group starts publishing publicly."
|
||||
items={QUICK_CHECKLIST}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<DocsSection
|
||||
id="next-steps"
|
||||
eyebrow="Keep going"
|
||||
title="Next steps"
|
||||
summary="Once the first Group exists and the first publish is clear, move into the next surface that helps your team actually operate."
|
||||
>
|
||||
<QuickstartNextSteps items={nextSteps} />
|
||||
</DocsSection>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
110
resources/js/Pages/Group/GroupReleaseShow.jsx
Normal file
110
resources/js/Pages/Group/GroupReleaseShow.jsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import React from 'react'
|
||||
import { usePage } from '@inertiajs/react'
|
||||
import SeoHead from '../../components/seo/SeoHead'
|
||||
|
||||
function ArtworkGrid({ artworks }) {
|
||||
if (!Array.isArray(artworks) || artworks.length === 0) {
|
||||
return <p className="mt-4 text-sm text-slate-400">No linked artworks yet.</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-4 grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{artworks.map((artwork) => (
|
||||
<a key={artwork.id} href={artwork.url} className="overflow-hidden rounded-[24px] border border-white/10 bg-black/20 transition hover:border-white/20">
|
||||
{artwork.thumb ? <img src={artwork.thumb} alt={artwork.title} className="aspect-[4/3] w-full object-cover" /> : null}
|
||||
<div className="p-4">
|
||||
<h3 className="text-base font-semibold text-white">{artwork.title}</h3>
|
||||
<p className="mt-1 text-sm text-slate-400">{artwork.author || 'Artwork'}</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function GroupReleaseShow() {
|
||||
const { props } = usePage()
|
||||
const group = props.group || {}
|
||||
const release = props.release || {}
|
||||
const contributors = Array.isArray(release.contributors) ? release.contributors : []
|
||||
const milestones = Array.isArray(release.milestones) ? release.milestones : []
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.16),_transparent_28%),linear-gradient(180deg,_#020617_0%,_#02040a_100%)] px-4 py-10 sm:px-6 lg:px-8">
|
||||
<SeoHead seo={props.seo || {}} title={`${release.title || group.name} - Skinbase`} description={release.summary || release.description || group.headline || 'Group release'} />
|
||||
<div className="mx-auto max-w-6xl space-y-8">
|
||||
<section className="overflow-hidden rounded-[32px] border border-white/10 bg-white/[0.03]">
|
||||
{release.cover_url ? <img src={release.cover_url} alt={release.title} className="h-64 w-full object-cover" /> : <div className="h-44 bg-white/[0.03]" />}
|
||||
<div className="p-6">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<a href={group.urls?.public} className="text-sm font-semibold text-sky-200">{group.name}</a>
|
||||
{release.status ? <span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-300">{release.status}</span> : null}
|
||||
{release.current_stage ? <span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-300">{release.current_stage}</span> : null}
|
||||
</div>
|
||||
<h1 className="mt-4 text-4xl font-semibold text-white">{release.title}</h1>
|
||||
{release.summary ? <p className="mt-4 max-w-3xl text-sm leading-7 text-slate-300">{release.summary}</p> : null}
|
||||
<div className="mt-5 flex flex-wrap gap-4 text-xs text-slate-400">
|
||||
{release.released_at ? <span>Released {new Date(release.released_at).toLocaleDateString()}</span> : null}
|
||||
{release.planned_release_at ? <span>Planned {new Date(release.planned_release_at).toLocaleDateString()}</span> : null}
|
||||
{release.lead?.name || release.lead?.username ? <span>Lead: {release.lead?.name || release.lead?.username}</span> : null}
|
||||
<span>{release.counts?.artworks || 0} artworks</span>
|
||||
<span>{release.counts?.contributors || 0} contributors</span>
|
||||
<span>{release.counts?.milestones || 0} milestones</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="grid gap-8 xl:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]">
|
||||
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-2xl font-semibold text-white">Overview</h2>
|
||||
<p className="mt-4 text-sm leading-7 text-slate-300">{release.description || 'No long-form release description yet.'}</p>
|
||||
{release.release_notes ? <div className="mt-6 rounded-[24px] border border-white/10 bg-black/20 p-4"><div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Release notes</div><div className="mt-3 whitespace-pre-wrap text-sm leading-7 text-slate-300">{release.release_notes}</div></div> : null}
|
||||
<ArtworkGrid artworks={release.artworks} />
|
||||
</section>
|
||||
|
||||
<div className="space-y-8">
|
||||
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-2xl font-semibold text-white">Links</h2>
|
||||
<div className="mt-4 space-y-3">
|
||||
{release.linked_project?.url ? <a href={release.linked_project.url} className="block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white"><div className="font-semibold">{release.linked_project.title}</div><div className="mt-1 text-xs uppercase tracking-[0.16em] text-slate-400">Linked project</div></a> : null}
|
||||
{release.linked_collection?.url ? <a href={release.linked_collection.url} className="block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white"><div className="font-semibold">{release.linked_collection.title}</div><div className="mt-1 text-xs uppercase tracking-[0.16em] text-slate-400">Linked collection</div></a> : null}
|
||||
{release.featured_artwork ? <div className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white"><div className="font-semibold">{release.featured_artwork.title}</div><div className="mt-1 text-xs uppercase tracking-[0.16em] text-slate-400">Featured artwork</div></div> : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-2xl font-semibold text-white">Contributors</h2>
|
||||
<div className="mt-4 space-y-3">
|
||||
{contributors.length > 0 ? contributors.map((contributor) => (
|
||||
<div key={contributor.id} className="flex items-center gap-3 rounded-2xl border border-white/10 bg-black/20 px-4 py-3">
|
||||
{contributor.avatar_url ? <img src={contributor.avatar_url} alt={contributor.name || contributor.username} className="h-11 w-11 rounded-2xl object-cover" /> : <div className="flex h-11 w-11 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">{contributor.name || contributor.username}</div>
|
||||
<div className="text-xs uppercase tracking-[0.16em] text-slate-400">{contributor.role_label || 'Contributor'}</div>
|
||||
</div>
|
||||
</div>
|
||||
)) : <p className="text-sm text-slate-400">No contributor credits yet.</p>}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-2xl font-semibold text-white">Milestones</h2>
|
||||
<div className="mt-4 space-y-3">
|
||||
{milestones.length > 0 ? milestones.map((milestone) => (
|
||||
<div key={milestone.id} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="font-semibold text-white">{milestone.title}</div>
|
||||
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-300">{milestone.status}</span>
|
||||
</div>
|
||||
{milestone.summary ? <p className="mt-2 text-sm text-slate-400">{milestone.summary}</p> : null}
|
||||
<div className="mt-2 text-xs text-slate-500">{milestone.owner?.name || milestone.owner?.username || 'No owner'}{milestone.due_date ? ` • due ${milestone.due_date}` : ''}</div>
|
||||
</div>
|
||||
)) : <p className="text-sm text-slate-400">No milestones defined yet.</p>}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
987
resources/js/Pages/Group/GroupShow.jsx
Normal file
987
resources/js/Pages/Group/GroupShow.jsx
Normal file
@@ -0,0 +1,987 @@
|
||||
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()
|
||||
}
|
||||
|
||||
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 <p className="mt-5 text-sm text-slate-400">{emptyLabel}</p>
|
||||
}
|
||||
|
||||
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="overflow-hidden rounded-[24px] border border-white/10 bg-black/20 transition hover:border-white/20">
|
||||
{artwork.thumb ? <img src={artwork.thumb} alt={artwork.title} className="aspect-[4/3] w-full object-cover" /> : null}
|
||||
<div className="p-4">
|
||||
<h3 className="text-base font-semibold text-white">{artwork.title}</h3>
|
||||
<p className="mt-1 text-sm text-slate-400">{artwork.author}</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CollectionGrid({ collections, emptyLabel = 'No collections yet.' }) {
|
||||
if (!Array.isArray(collections) || collections.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">
|
||||
{collections.map((collection) => (
|
||||
<a key={collection.id} href={collection.url} className="rounded-[24px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-white">{collection.title}</h3>
|
||||
<p className="mt-2 text-sm text-slate-300">{collection.summary || collection.description_excerpt || 'Collection'}</p>
|
||||
</div>
|
||||
{collection.is_featured ? <span className="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>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CompactCardGrid({ items, emptyLabel, badgeKey = 'status' }) {
|
||||
if (!Array.isArray(items) || items.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">
|
||||
{items.map((item) => (
|
||||
<a key={item.id} href={item.url} className="rounded-[24px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h3 className="text-base font-semibold text-white">{item.title}</h3>
|
||||
{item[badgeKey] ? <span className="rounded-full border border-white/10 bg-white/[0.04] px-2 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>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ReleaseGrid({ releases, emptyLabel = 'No public releases yet.' }) {
|
||||
if (!Array.isArray(releases) || releases.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">
|
||||
{releases.map((release) => (
|
||||
<a key={release.id} href={release.url} className="overflow-hidden rounded-[24px] border border-white/10 bg-black/20 transition hover:border-white/20">
|
||||
{release.cover_url ? <img src={release.cover_url} alt={release.title} className="aspect-[4/3] w-full object-cover" /> : <div className="flex aspect-[4/3] items-center justify-center bg-white/[0.03] text-slate-500"><i className="fa-solid fa-rocket text-2xl" /></div>}
|
||||
<div className="p-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="rounded-full border border-white/10 bg-white/[0.04] px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-300">{release.status}</span>
|
||||
<span className="rounded-full border border-white/10 bg-white/[0.04] px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-300">{release.current_stage}</span>
|
||||
</div>
|
||||
<h3 className="mt-3 text-base font-semibold text-white">{release.title}</h3>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-300">{release.summary || 'Release overview and linked artworks.'}</p>
|
||||
<div className="mt-3 text-xs text-slate-500">{release.counts?.artworks || 0} artworks • {release.counts?.contributors || 0} contributors • {release.counts?.milestones || 0} milestones</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="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/70">Leadership</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold text-white">Owner and admins</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 grid gap-3 sm:grid-cols-2">
|
||||
{leadership.map((member) => (
|
||||
<a key={member.id} href={member.profile_url || '#'} className="flex items-center gap-3 rounded-[24px] border border-white/10 bg-black/20 px-4 py-4 transition hover:border-white/20">
|
||||
{member.avatar_url ? <img src={member.avatar_url} alt={member.name || member.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 className="min-w-0">
|
||||
<div className="truncate font-semibold text-white">{member.name || member.username}</div>
|
||||
<div className="text-xs uppercase tracking-[0.16em] text-slate-400">{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 toneClasses = {
|
||||
sky: 'border-sky-300/20 bg-sky-300/10 text-sky-100',
|
||||
emerald: 'border-emerald-300/20 bg-emerald-300/10 text-emerald-100',
|
||||
amber: 'border-amber-300/20 bg-amber-300/10 text-amber-100',
|
||||
violet: 'border-violet-300/20 bg-violet-300/10 text-violet-100',
|
||||
}
|
||||
|
||||
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">Trust signals</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold text-white">How this group shows up</h2>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{signals.map((signal) => <span key={signal.key} className={`rounded-full border px-3 py-2 text-sm font-semibold ${toneClasses[signal.tone] || 'border-white/10 bg-white/[0.04] text-white'}`}>{signal.label}</span>)}
|
||||
</div>
|
||||
<div className="mt-5 space-y-3">
|
||||
{signals.map((signal) => <div key={`${signal.key}-reason`} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-4"><div className="font-semibold text-white">{signal.label}</div><p className="mt-2 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="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-100/80">Badges</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold text-white">Earned group signals</h2>
|
||||
<div className="mt-5 grid gap-3">
|
||||
{badges.map((badge) => (
|
||||
<div key={badge.key} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="font-semibold text-white">{badge.label}</div>
|
||||
{badge.awarded_at ? <div className="text-xs text-slate-500">{new Date(badge.awarded_at).toLocaleDateString()}</div> : null}
|
||||
</div>
|
||||
<p className="mt-2 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="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">Contributors</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold text-white">Trusted collaborators</h2>
|
||||
<div className="mt-5 space-y-3">
|
||||
{contributors.map((entry) => (
|
||||
<a key={entry.user?.id} href={entry.user?.profile_url || '#'} className="flex gap-3 rounded-[24px] border border-white/10 bg-black/20 px-4 py-4 transition hover:border-white/20">
|
||||
{entry.user?.avatar_url ? <img src={entry.user.avatar_url} alt={entry.user?.name || entry.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 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="rounded-full border border-emerald-300/20 bg-emerald-300/10 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-emerald-100">Trusted</span> : null}
|
||||
</div>
|
||||
{entry.summary ? <p className="mt-1 text-sm text-slate-400">{entry.summary}</p> : null}
|
||||
<div className="mt-2 text-xs text-slate-500">{entry.counts?.releases || 0} releases • {entry.counts?.credited_artworks || 0} artworks • {entry.counts?.projects || 0} projects</div>
|
||||
{Array.isArray(entry.badges) && entry.badges.length > 0 ? <div className="mt-3 flex flex-wrap gap-2">{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="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-100/80">Highlights</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold text-white">Featured artworks</h2>
|
||||
</div>
|
||||
<a href={`${group.urls?.public}/artworks`} className="text-sm font-semibold text-sky-200">Browse all</a>
|
||||
</div>
|
||||
<ArtworkGrid artworks={featuredArtworks} emptyLabel="No featured artworks yet." />
|
||||
</section>
|
||||
|
||||
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/70">Latest work</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold text-white">Latest artworks</h2>
|
||||
</div>
|
||||
<a href={`${group.urls?.public}/artworks`} className="text-sm font-semibold text-sky-200">View archive</a>
|
||||
</div>
|
||||
<ArtworkGrid artworks={artworks.slice(0, 6)} emptyLabel="No published artworks yet." />
|
||||
</section>
|
||||
|
||||
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-100/80">Pipeline</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold text-white">Recent releases</h2>
|
||||
</div>
|
||||
<a href={`${group.urls?.public}/releases`} className="text-sm font-semibold text-sky-200">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="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-100/80">Curated</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold text-white">Featured collections</h2>
|
||||
</div>
|
||||
<a href={`${group.urls?.public}/collections`} className="text-sm font-semibold text-sky-200">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="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-100/80">Pinned post</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold text-white">{group.pinned_post.title}</h2>
|
||||
<p className="mt-4 text-sm leading-7 text-slate-300">{group.pinned_post.excerpt || 'Read the latest pinned update from this group.'}</p>
|
||||
<a href={group.pinned_post.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">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="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">Resources</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold text-white">Shared downloads</h2>
|
||||
<AssetGrid assets={assets.slice(0, 3)} />
|
||||
</section>
|
||||
|
||||
<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">Public feed</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold text-white">Recent activity</h2>
|
||||
<ActivityFeed items={activity.slice(0, 4)} />
|
||||
</section>
|
||||
|
||||
<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">About</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold text-white">About {group.name}</h2>
|
||||
<p className="mt-5 text-sm leading-7 text-slate-300">{group.bio || 'No long-form description yet.'}</p>
|
||||
<div className="mt-5 flex flex-wrap gap-3 text-xs text-slate-400">
|
||||
{group.founded_at ? <span>Founded {new Date(group.founded_at).toLocaleDateString()}</span> : null}
|
||||
{group.type ? <span>{group.type}</span> : null}
|
||||
{group.website_url ? <a href={group.website_url} className="text-sky-200 underline underline-offset-4">Website</a> : null}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{section === 'artworks' ? (
|
||||
<section className="mt-8 rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold text-white">Artworks</h2>
|
||||
<p className="mt-2 text-sm text-slate-400">Filter the group archive by title or contributor credit label, then change the sort order.</p>
|
||||
</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 rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-2xl font-semibold text-white">Collections</h2>
|
||||
<CollectionGrid collections={collections} emptyLabel="No collections yet." />
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{section === 'posts' ? (
|
||||
<section className="mt-8 rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-2xl font-semibold text-white">Posts</h2>
|
||||
<div className="mt-5 grid gap-4 md:grid-cols-2">
|
||||
{posts.length > 0 ? posts.map((post) => (
|
||||
<a key={post.id} href={post.url} className="rounded-[24px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">{post.type}</div>
|
||||
{post.is_pinned ? <span className="rounded-full border border-amber-300/20 bg-amber-400/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-amber-100">Pinned</span> : null}
|
||||
</div>
|
||||
<h3 className="mt-2 text-lg font-semibold text-white">{post.title}</h3>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-300">{post.excerpt || 'Open the post to read more.'}</p>
|
||||
</a>
|
||||
)) : <p className="text-sm text-slate-400">No posts published yet.</p>}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{section === 'projects' ? (
|
||||
<section className="mt-8 rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-2xl font-semibold text-white">Projects</h2>
|
||||
<p className="mt-2 text-sm text-slate-400">Structured releases, collaboration hubs, and production pages published by this group.</p>
|
||||
<CompactCardGrid items={projects} emptyLabel="No public projects yet." />
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{section === 'releases' ? (
|
||||
<section className="mt-8 rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-2xl font-semibold text-white">Releases</h2>
|
||||
<p className="mt-2 text-sm text-slate-400">Published drops, milestone pipelines, and linked showcases from this group.</p>
|
||||
<ReleaseGrid releases={releases} emptyLabel="No public releases yet." />
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{section === 'challenges' ? (
|
||||
<section className="mt-8 rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-2xl font-semibold text-white">Challenges</h2>
|
||||
<p className="mt-2 text-sm text-slate-400">Current and past prompts, internal sprints, and public-facing challenge runs.</p>
|
||||
<CompactCardGrid items={challenges} emptyLabel="No public challenges yet." />
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{section === 'events' ? (
|
||||
<section className="mt-8 rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-2xl font-semibold text-white">Events</h2>
|
||||
<p className="mt-2 text-sm text-slate-400">Launches, milestones, streams, and other moments on the group timeline.</p>
|
||||
<CompactCardGrid items={events} emptyLabel="No public events yet." badgeKey="event_type" />
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{section === 'activity' ? (
|
||||
<section className="mt-8 rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-2xl font-semibold text-white">Activity</h2>
|
||||
<p className="mt-2 text-sm text-slate-400">Public milestones from posts, releases, events, member changes, and challenge highlights.</p>
|
||||
<ActivityFeed items={activity} />
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{section === 'members' ? (
|
||||
<section className="mt-8 rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-2xl font-semibold text-white">Members</h2>
|
||||
<div className="mt-6 grid gap-8">
|
||||
{[
|
||||
['Owner', groupedMembers.owner],
|
||||
['Admins', groupedMembers.admins],
|
||||
['Editors', groupedMembers.editors],
|
||||
['Contributors', groupedMembers.contributors],
|
||||
].map(([label, bucket]) => (
|
||||
bucket.length > 0 ? (
|
||||
<section key={label}>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h3 className="text-lg font-semibold text-white">{label}</h3>
|
||||
<span className="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="flex items-center gap-3 rounded-[24px] border border-white/10 bg-black/20 px-4 py-4 transition hover:border-white/20">
|
||||
{member.user?.avatar_url ? <img src={member.user.avatar_url} alt={member.user.name || member.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 className="min-w-0">
|
||||
<div className="truncate font-semibold text-white">{member.user?.name || member.user?.username}</div>
|
||||
<div className="text-xs uppercase tracking-[0.16em] text-slate-400">{member.role_label || member.role}</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{section === 'about' ? (
|
||||
<section className="mt-8 rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-2xl font-semibold text-white">About</h2>
|
||||
<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="text-sky-200 underline underline-offset-4">{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="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs font-semibold text-white">{link.label}</a>)}</div> : null}
|
||||
{group.founded_at ? <p>Founded: {new Date(group.founded_at).toLocaleDateString()}</p> : null}
|
||||
{group.type ? <p>Type: {group.type}</p> : null}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
67
resources/js/Pages/Group/__tests__/GroupShow.test.jsx
Normal file
67
resources/js/Pages/Group/__tests__/GroupShow.test.jsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from 'react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import GroupShow from '../GroupShow'
|
||||
|
||||
let pageMock = { props: {} }
|
||||
|
||||
vi.mock('@inertiajs/react', () => ({
|
||||
usePage: () => pageMock,
|
||||
}))
|
||||
|
||||
vi.mock('../../../components/seo/SeoHead', () => ({
|
||||
default: () => null,
|
||||
}))
|
||||
|
||||
vi.mock('../../../hooks/useWebShare', () => ({
|
||||
default: () => ({ share: vi.fn() }),
|
||||
}))
|
||||
|
||||
describe('GroupShow public page', () => {
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders the public group hero, tabs, and grouped members', () => {
|
||||
pageMock = {
|
||||
props: {
|
||||
section: 'members',
|
||||
group: {
|
||||
id: 1,
|
||||
name: 'Warp Collective',
|
||||
headline: 'Retro visual lab',
|
||||
visibility: 'public',
|
||||
status: 'active',
|
||||
counts: { artworks: 4, collections: 2, members: 4, followers: 10 },
|
||||
urls: { public: '/groups/warp-collective', follow: '/groups/warp-collective/follow', unfollow: '/groups/warp-collective/follow' },
|
||||
viewer: { is_following: false },
|
||||
},
|
||||
featuredArtworks: [],
|
||||
artworks: [],
|
||||
featuredCollections: [],
|
||||
collections: [],
|
||||
leadership: [],
|
||||
members: [
|
||||
{ id: 1, role: 'owner', role_label: 'owner', user: { name: 'Owner', username: 'owner', profile_url: '/@owner', avatar_url: null } },
|
||||
{ id: 2, role: 'admin', role_label: 'admin', user: { name: 'Admin', username: 'admin', profile_url: '/@admin', avatar_url: null } },
|
||||
{ id: 3, role: 'editor', role_label: 'editor', user: { name: 'Editor', username: 'editor', profile_url: '/@editor', avatar_url: null } },
|
||||
{ id: 4, role: 'contributor', role_label: 'contributor', user: { name: 'Contributor', username: 'contributor', profile_url: '/@contributor', avatar_url: null } },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
render(<GroupShow />)
|
||||
|
||||
expect(screen.getByRole('heading', { name: /warp collective/i })).not.toBeNull()
|
||||
expect(screen.getByRole('link', { name: 'overview' })).not.toBeNull()
|
||||
expect(screen.getByRole('link', { name: 'artworks' })).not.toBeNull()
|
||||
expect(screen.getByRole('link', { name: 'collections' })).not.toBeNull()
|
||||
expect(screen.getByRole('link', { name: 'members' })).not.toBeNull()
|
||||
expect(screen.getByRole('link', { name: 'about' })).not.toBeNull()
|
||||
expect(screen.getByRole('heading', { name: 'Owner' })).not.toBeNull()
|
||||
expect(screen.getByRole('heading', { name: 'Admins' })).not.toBeNull()
|
||||
expect(screen.getByRole('heading', { name: 'Editors' })).not.toBeNull()
|
||||
expect(screen.getByRole('heading', { name: 'Contributors' })).not.toBeNull()
|
||||
})
|
||||
})
|
||||
484
resources/js/Pages/Group/groupFaqContent.js
Normal file
484
resources/js/Pages/Group/groupFaqContent.js
Normal file
@@ -0,0 +1,484 @@
|
||||
export const FAQ_CATEGORIES = [
|
||||
{
|
||||
id: 'basics',
|
||||
label: 'Basics',
|
||||
title: 'Basics',
|
||||
summary: 'Start here if you need the fastest explanation of what Groups are and when they make sense.',
|
||||
items: [
|
||||
{
|
||||
question: 'What is a Group?',
|
||||
paragraphs: [
|
||||
'A Group is a shared creative identity for teams, collectives, projects, and recurring collaboration. It gives multiple creators one public home for work, updates, and shared activity.',
|
||||
'It is meant for collaboration, not as a replacement for your personal profile.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'What is the difference between a personal profile and a Group?',
|
||||
paragraphs: [
|
||||
'A personal profile is your individual identity, portfolio, and reputation. A Group is the team or shared identity layer.',
|
||||
'Both can exist side by side. You can keep publishing personally while also publishing collaborative work under a Group.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Should I create a Group?',
|
||||
paragraphs: [
|
||||
'Create one if you collaborate regularly, want a shared public brand, or need shared roles and publishing workflows.',
|
||||
'If you only publish solo work and do not need a team identity yet, you can stay on your personal profile for now.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Can I still publish personally if I also use a Group?',
|
||||
paragraphs: [
|
||||
'Yes. Many creators use both. Personal publishing is for individual work, while Group publishing is for collaborative work or a shared brand.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Can a Group exist with only one member?',
|
||||
paragraphs: [
|
||||
'Yes, if the product allows it, but it is most useful when there is a real shared identity or collaboration reason behind it.',
|
||||
'If it is only being used to rename personal work, a personal profile may still be the simpler choice.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'roles-and-permissions',
|
||||
label: 'Roles & Permissions',
|
||||
title: 'Roles & permissions',
|
||||
summary: 'These answers explain who can do what and why role differences exist inside a Group.',
|
||||
items: [
|
||||
{
|
||||
question: 'What does the Owner role do?',
|
||||
paragraphs: [
|
||||
'Owner is the highest-trust role. Owners control sensitive settings, membership structure, and the overall direction of the Group.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'What does the Admin role do?',
|
||||
paragraphs: [
|
||||
'Admins usually help manage day-to-day Group operations, member access, and important content workflows.',
|
||||
'This role should stay limited to people the Group deeply trusts.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'What does the Editor role do?',
|
||||
paragraphs: [
|
||||
'Editors are usually the best fit for people who help manage content, publishing, reviews, releases, or coordination without needing full Group control.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'What does the Contributor role do?',
|
||||
paragraphs: [
|
||||
'Contributors participate in the creative side of the Group without needing broad access to settings or member management.',
|
||||
'For many teams, this is the right starting role for most collaborators.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Who can invite members?',
|
||||
paragraphs: [
|
||||
'Usually Owners and Admins, depending on the Group setup. If you do not see invite controls, your role probably does not include member management.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Who can change member roles?',
|
||||
paragraphs: [
|
||||
'Usually Owners and sometimes Admins. This depends on the Group’s trust model and any role restrictions already in place.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Who can publish as the Group?',
|
||||
paragraphs: [
|
||||
'That depends on the Group role and workflow. Owners and Admins often can. Editors often can. Contributors may submit drafts without publishing directly if the team uses approvals.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Why can’t I do something another member can do?',
|
||||
paragraphs: [
|
||||
'Roles are not identical. One person may have a higher role or a permission override that gives access you do not have.',
|
||||
'If you are unsure, ask an Owner or Admin what your role is meant to cover.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Should I give lots of people Admin access?',
|
||||
paragraphs: [
|
||||
'Usually no. Keep high-level roles limited. It is easier to add trust later than clean up a Group where too many people can change everything.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Can permissions be customized?',
|
||||
paragraphs: [
|
||||
'In some cases, yes. Some Groups may use permission overrides on top of the main role system.',
|
||||
'If your team is new, it is usually better to keep the role model simple first and only customize later when there is a clear need.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'publishing-and-credit',
|
||||
label: 'Publishing & Credit',
|
||||
title: 'Publishing & contributor credit',
|
||||
summary: 'This section covers the biggest source of user confusion: how shared identity and individual attribution work together.',
|
||||
items: [
|
||||
{
|
||||
question: 'What does “publish as Group” mean?',
|
||||
paragraphs: [
|
||||
'It means the work appears publicly under the Group identity rather than under a personal profile.',
|
||||
'That does not erase individual authorship or responsibility for the work.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Will my name still appear if I publish under a Group?',
|
||||
paragraphs: [
|
||||
'Yes. Group publishing is designed to preserve individual credit and accountability, not hide it.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'What is the difference between Published by, Uploaded by, Primary author, and Contributors?',
|
||||
paragraphs: [
|
||||
'Published by is the public identity the work appears under. Uploaded by is the person who handled the upload or final publish step. Primary author is the main author of the work. Contributors are additional people who made meaningful creative contributions.',
|
||||
],
|
||||
example: [
|
||||
{ label: 'Published by', value: 'Warlock' },
|
||||
{ label: 'Uploaded by', value: 'Gregor' },
|
||||
{ label: 'Primary author', value: 'Gregor' },
|
||||
{ label: 'Contributors', value: 'Denis, Paula' },
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Who should be listed as Primary author?',
|
||||
paragraphs: [
|
||||
'The primary author should be the person who should clearly be understood as the main author of the work.',
|
||||
'Do not choose this field based only on who clicked Publish.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Should all contributors be credited?',
|
||||
paragraphs: [
|
||||
'Yes, if they made meaningful creative contributions. Clear credit keeps the Group trustworthy and helps avoid internal confusion later.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Can a Group publish an artwork while still showing who made it?',
|
||||
paragraphs: [
|
||||
'Yes. That is one of the main points of Group publishing: shared identity on the public surface, clear attribution for the humans behind the work.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Can I publish both personal and Group artworks?',
|
||||
paragraphs: [
|
||||
'Yes. Many creators do both. The important thing is choosing the correct context before the final publish step.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Why does the platform keep individual credit visible?',
|
||||
paragraphs: [
|
||||
'Because collaboration should not erase accountability or authorship. The Group represents the shared identity, but people still deserve clear credit for the work they did.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'What should we do if contributor credit is wrong?',
|
||||
paragraphs: [
|
||||
'Fix it quickly. Review who uploaded the work, who authored it, and who contributed before making changes publicly.',
|
||||
'If there is disagreement inside the team, resolve that first so the public record reflects a clear shared decision.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Can contributor credit be changed later?',
|
||||
paragraphs: [
|
||||
'In many cases, yes, depending on your Group permissions and workflow. The best habit is to get it right before publishing so you do not have to correct it afterward.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'members-and-invites',
|
||||
label: 'Members & Invites',
|
||||
title: 'Members, invites, and join requests',
|
||||
summary: 'Use these answers when you need to manage who gets access, what role they should have, and what happens when the team changes.',
|
||||
items: [
|
||||
{
|
||||
question: 'How do I invite someone to a Group?',
|
||||
paragraphs: [
|
||||
'Open Group Studio, go to the member or invitation controls, choose the right role, and send the invite once you know what access that person actually needs.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Can I remove a member later?',
|
||||
paragraphs: [
|
||||
'Yes, if your role allows member management. Owners and authorized admins can usually update access, revoke invites, or remove active members.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'What happens if a member leaves the Group?',
|
||||
paragraphs: [
|
||||
'Their active access can be removed, but that does not usually erase the history of work they already contributed to.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Can a former member still appear on older artworks they contributed to?',
|
||||
paragraphs: [
|
||||
'Yes. Older work may still show their contribution because that is part of the record of who helped make it.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Can people request to join a Group?',
|
||||
paragraphs: [
|
||||
'If the Group allows join requests or recruiting, yes. Otherwise access usually depends on direct invites from the team.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'What is recruitment mode?',
|
||||
paragraphs: [
|
||||
'Recruitment mode is the public-facing signal that a Group is looking for new collaborators. It helps teams describe what roles or skills they want and how people should reach out.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'How do I choose the right role for a new member?',
|
||||
paragraphs: [
|
||||
'Start from what they actually need to do right now. If you are unsure, start lower and promote later instead of giving broad access too early.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Can I change someone’s role later?',
|
||||
paragraphs: [
|
||||
'Yes, if your role allows it. Many teams adjust roles over time as trust, responsibility, or activity changes.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Why can’t I manage members?',
|
||||
paragraphs: [
|
||||
'Your role probably does not include member management. That level of access is usually kept to Owners and selected Admins.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Why can’t I see invite controls?',
|
||||
paragraphs: [
|
||||
'Invite controls are normally hidden if your role does not include them or if you are not operating inside the correct Group context.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'review-workflow',
|
||||
label: 'Workflow & Review',
|
||||
title: 'Review workflow and approvals',
|
||||
summary: 'These answers explain why some Groups use review queues and how that affects contributors and trusted publishers.',
|
||||
items: [
|
||||
{
|
||||
question: 'Why is my artwork in review?',
|
||||
paragraphs: [
|
||||
'Your Group may use a review-first workflow so contributors can submit work without publishing directly. That helps the team catch quality, context, or credit issues before something goes public.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Who can approve Group submissions?',
|
||||
paragraphs: [
|
||||
'Usually the people whose roles include review access, such as Owners, Admins, or selected Editors.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'What does “needs changes” mean?',
|
||||
paragraphs: [
|
||||
'It means the submission is not ready yet but may become ready after updates. It is a request to revise, not an automatic rejection.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Can contributors submit drafts without publishing directly?',
|
||||
paragraphs: [
|
||||
'Yes. That is a common Group workflow. Contributors hand work off for review while a trusted reviewer or publisher handles the final public step.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Why would a Group use a review queue?',
|
||||
paragraphs: [
|
||||
'Review queues help larger or more structured teams keep public quality high, coordinate releases, and catch mistakes before launch.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Do all Groups need approval workflow?',
|
||||
paragraphs: [
|
||||
'No. Small, trusted teams may prefer direct publishing. Review is useful when it solves a real quality or coordination problem.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Can trusted members publish directly?',
|
||||
paragraphs: [
|
||||
'Yes, if their role allows it. Many teams reserve direct publishing for trusted operators and use review for everyone else.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'What should I do if my submission was rejected?',
|
||||
paragraphs: [
|
||||
'Check the feedback first, then ask for clarification if needed. Treat rejection as workflow feedback, not as punishment.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'features-and-content-types',
|
||||
label: 'Features & Content Types',
|
||||
title: 'Group features and content types',
|
||||
summary: 'This section explains how the wider Group ecosystem fits together so users know what to start with and what to add later.',
|
||||
items: [
|
||||
{
|
||||
question: 'Can a Group create posts or announcements?',
|
||||
paragraphs: [
|
||||
'Yes. Posts are useful for release notes, updates, announcements, recruitment, or milestone communication.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'What are Group projects used for?',
|
||||
paragraphs: [
|
||||
'Projects are for structured collaboration. They give the team a shared place to organize work, milestones, linked content, and progress.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'What are Group challenges used for?',
|
||||
paragraphs: [
|
||||
'Challenges help run themed prompts, community events, or internal creative pushes that keep the Group active and focused.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'What are Group events used for?',
|
||||
paragraphs: [
|
||||
'Events are for launches, streams, showcases, meetups, release windows, or any time-based public moment the Group wants to anchor clearly.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'What is the shared asset library for?',
|
||||
paragraphs: [
|
||||
'The asset library stores shared resources, references, files, and internal materials so they do not get lost in scattered chat or personal storage.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'What are releases?',
|
||||
paragraphs: [
|
||||
'Releases package a major publication moment with a title, summary, contributors, milestones, notes, and linked work in one public surface.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Do all Groups need to use projects, challenges, events, or releases?',
|
||||
paragraphs: [
|
||||
'No. Start with the smallest set of tools that makes your workflow clearer. Not every Group needs every feature from day one.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'What should a simple Group use first?',
|
||||
paragraphs: [
|
||||
'Most simple Groups should begin with a clear profile, member roles, artworks, and occasional posts. Add more structure only when it solves a real problem.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'What should a more advanced Group use later?',
|
||||
paragraphs: [
|
||||
'As the Group grows, projects, releases, review queues, recruitment, challenges, events, and shared assets become more useful.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'troubleshooting',
|
||||
label: 'Troubleshooting',
|
||||
title: 'Troubleshooting',
|
||||
summary: 'Use these answers when something feels wrong, missing, or inconsistent. Most Group issues come down to context, role, or visibility.',
|
||||
items: [
|
||||
{
|
||||
question: 'I can’t publish as the Group. Why?',
|
||||
paragraphs: [
|
||||
'The usual reasons are the wrong context, insufficient permissions, inactive membership, or a Group state or policy restriction. Start by confirming you are inside Group Studio and that your role allows publishing.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'I don’t see Group Studio.',
|
||||
paragraphs: [
|
||||
'You may not be in the Group, may still have a pending invite, or may not be signed in. Accept the invitation first if one is waiting.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'I was invited, but I still can’t access what I expected.',
|
||||
paragraphs: [
|
||||
'Check whether the invitation was fully accepted and whether the content you expect is internal, role-limited, or review-limited.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'My role does not let me do what I need.',
|
||||
paragraphs: [
|
||||
'Your Group may be intentionally limiting that action to a higher-trust role. Ask an Owner or Admin whether your current role matches the work you are actually doing.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'I published under the wrong context. What should I do?',
|
||||
paragraphs: [
|
||||
'Review the affected content immediately. Confirm whether it should live under the personal profile or the Group, then correct it before more linked content builds around the mistake.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Contributor credit is incorrect. What should I do?',
|
||||
paragraphs: [
|
||||
'Check the publish record and confirm who was published under, who uploaded the work, who authored it, and who contributed. Fix the incorrect part instead of replacing everything blindly.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'I can’t manage members.',
|
||||
paragraphs: [
|
||||
'Member management is usually restricted to Owners and selected Admins. If you do not see those controls, your role probably does not include them.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'I can’t see internal Group assets or projects.',
|
||||
paragraphs: [
|
||||
'Those areas may be internal-only, visibility-limited, or restricted by role. Confirm that you are an active member and that your role is supposed to see that content.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'I don’t understand why I can’t approve submissions.',
|
||||
paragraphs: [
|
||||
'Approval access is usually reserved for trusted operators. If your role is Contributor or a limited Editor role, approvals may be intentionally hidden from you.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'Our Group page looks empty. What should we do first?',
|
||||
paragraphs: [
|
||||
'Start with the basics: complete the profile, upload branding, publish one strong piece, and add one meaningful update. A small amount of clear activity is better than a big empty shell.',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'We are not sure which role to assign someone. What should we do?',
|
||||
paragraphs: [
|
||||
'Base the role on what they need to do this month, not on what title sounds impressive. If you are unsure, start lower and adjust later.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const RELATED_HELP_ITEMS = [
|
||||
{
|
||||
eyebrow: 'Deep dive',
|
||||
title: 'Read the full Groups guide',
|
||||
body: 'Use the full documentation for broader reference, advanced workflows, FAQ overlap, and deeper best practices.',
|
||||
linkKey: 'full_documentation',
|
||||
tone: 'white',
|
||||
},
|
||||
{
|
||||
eyebrow: 'Start here',
|
||||
title: 'Open the Groups Quickstart',
|
||||
body: 'Use the shorter onboarding path if you want the fastest route to create a Group and publish correctly.',
|
||||
linkKey: 'quickstart',
|
||||
tone: 'amber',
|
||||
},
|
||||
{
|
||||
eyebrow: 'Operate',
|
||||
title: 'Open Group Studio',
|
||||
body: 'Jump into Studio if your next step is inviting members, reviewing content, or working inside the Group context.',
|
||||
linkKey: 'group_studio',
|
||||
tone: 'sky',
|
||||
},
|
||||
{
|
||||
eyebrow: 'Create',
|
||||
title: 'Create a Group',
|
||||
body: 'If the FAQ answered the basics and you are ready to move, start the creation flow directly.',
|
||||
linkKey: 'create_group',
|
||||
tone: 'white',
|
||||
},
|
||||
]
|
||||
300
resources/js/Pages/Group/groupHelpContent.js
Normal file
300
resources/js/Pages/Group/groupHelpContent.js
Normal file
@@ -0,0 +1,300 @@
|
||||
export const SECTION_ITEMS = [
|
||||
{ id: 'introduction', label: 'Introduction' },
|
||||
{ id: 'what-are-groups', label: 'What are Groups?' },
|
||||
{ id: 'why-use-a-group', label: 'Why use a Group?' },
|
||||
{ id: 'when-to-create-a-group', label: 'When should you create one?' },
|
||||
{ id: 'how-groups-work', label: 'How Groups work' },
|
||||
{ id: 'roles-and-permissions', label: 'Roles and permissions' },
|
||||
{ id: 'creating-a-group', label: 'Creating a Group' },
|
||||
{ id: 'public-group-page', label: 'Public Group page' },
|
||||
{ id: 'group-studio', label: 'Group Studio' },
|
||||
{ id: 'publishing-as-a-group', label: 'Publishing as a Group' },
|
||||
{ id: 'contributor-credit', label: 'Contributor credit' },
|
||||
{ id: 'member-management', label: 'Invites and team management' },
|
||||
{ id: 'review-workflow', label: 'Review workflow' },
|
||||
{ id: 'group-features', label: 'Projects, posts, events, releases' },
|
||||
{ id: 'tips-and-best-practices', label: 'Tips and best practices' },
|
||||
{ id: 'common-mistakes', label: 'Common mistakes' },
|
||||
{ id: 'suggested-workflows', label: 'Suggested workflows' },
|
||||
{ id: 'faq', label: 'FAQ' },
|
||||
{ id: 'troubleshooting', label: 'Troubleshooting' },
|
||||
{ id: 'need-help', label: 'Need help?' },
|
||||
]
|
||||
|
||||
export const BENEFITS = [
|
||||
'Publish under a shared name without hiding the people behind the work.',
|
||||
'Separate team identity from personal portfolios when a project needs its own public home.',
|
||||
'Manage roles, approvals, and release workflows in one place.',
|
||||
'Run projects, posts, challenges, events, assets, and releases under the same umbrella.',
|
||||
'Recruit collaborators through a Group page instead of repeating the same pitch in DMs.',
|
||||
'Keep contributor credit visible even when the final publish surface is the Group.',
|
||||
]
|
||||
|
||||
export const GOOD_FIT = [
|
||||
'You collaborate with two or more people regularly.',
|
||||
'You want a shared public brand for a studio, crew, or collective.',
|
||||
'You publish themed drops, packs, showcases, or release-driven work.',
|
||||
'You need shared collections, posts, assets, or release notes.',
|
||||
'You want role-based access instead of everyone sharing one account.',
|
||||
]
|
||||
|
||||
export const NOT_YET = [
|
||||
'You only publish solo work and do not need a separate identity.',
|
||||
'You are still testing ideas and not ready to manage members.',
|
||||
'You want a Group only to rename personal work without collaboration value.',
|
||||
'You do not want shared workflow, shared publishing context, or shared moderation responsibility.',
|
||||
]
|
||||
|
||||
export const ROLE_TABLE = {
|
||||
columns: [
|
||||
{ key: 'role', label: 'Role' },
|
||||
{ key: 'settings', label: 'Manage settings' },
|
||||
{ key: 'members', label: 'Invite and change roles' },
|
||||
{ key: 'publishing', label: 'Publish content' },
|
||||
{ key: 'review', label: 'Review submissions' },
|
||||
{ key: 'workflow', label: 'Manage posts, projects, events, releases' },
|
||||
{ key: 'assets', label: 'Manage assets and shared resources' },
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
id: 'owner',
|
||||
role: 'Owner',
|
||||
settings: 'Full control over branding, settings, membership policy, and archive actions.',
|
||||
members: 'Can invite, remove, promote, transfer ownership, and approve the overall structure.',
|
||||
publishing: 'Can publish directly and define the team workflow.',
|
||||
review: 'Can always review, approve, request changes, or reject.',
|
||||
workflow: 'Full access to posts, projects, challenges, events, releases, and reputation.',
|
||||
assets: 'Full access.',
|
||||
},
|
||||
{
|
||||
id: 'admin',
|
||||
role: 'Admin',
|
||||
settings: 'Can usually manage day-to-day settings and operations.',
|
||||
members: 'Can invite and manage most members, but should not be handed out casually.',
|
||||
publishing: 'Usually yes.',
|
||||
review: 'Usually yes.',
|
||||
workflow: 'Usually full operational access across content areas.',
|
||||
assets: 'Usually full access.',
|
||||
},
|
||||
{
|
||||
id: 'editor',
|
||||
role: 'Editor',
|
||||
settings: 'Usually limited or no access to sensitive settings.',
|
||||
members: 'Usually cannot change member roles unless explicitly allowed.',
|
||||
publishing: 'Often yes, depending on your workflow.',
|
||||
review: 'Often yes when the team uses review queues.',
|
||||
workflow: 'Good fit for content managers, release coordinators, and project leads.',
|
||||
assets: 'Often yes.',
|
||||
},
|
||||
{
|
||||
id: 'contributor',
|
||||
role: 'Contributor',
|
||||
settings: 'No sensitive settings access.',
|
||||
members: 'Cannot manage team structure.',
|
||||
publishing: 'Usually submits drafts instead of publishing directly.',
|
||||
review: 'Usually no.',
|
||||
workflow: 'Best for creative collaborators who need to contribute without running the Group.',
|
||||
assets: 'Limited to what the Group makes available.',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const CREATE_STEPS = [
|
||||
{ title: 'Open Groups in Studio', description: 'Go to Group Studio from your main Studio area, then choose Create Group.' },
|
||||
{ title: 'Choose a clear name and slug', description: 'Pick a name people can remember and a slug that still makes sense a year from now.' },
|
||||
{ title: 'Add branding', description: 'Upload a recognizable avatar or logo and a cover image that gives the Group a clear visual identity.' },
|
||||
{ title: 'Write a short headline and description', description: 'Tell visitors what the Group makes, who it is for, and what kind of collaboration to expect.' },
|
||||
{ title: 'Set visibility', description: 'Choose whether the Group should be public, unlisted, or private based on how ready you are.' },
|
||||
{ title: 'Create the Group', description: 'Finish setup, then review the public page so the branding and copy feel intentional.' },
|
||||
{ title: 'Invite your first members', description: 'Bring in owners, admins, editors, or contributors based on the work each person will actually do.' },
|
||||
{ title: 'Decide on your workflow', description: 'Choose whether the team should use direct publishing, review-first publishing, projects, releases, or lightweight milestones.' },
|
||||
{ title: 'Publish with care', description: 'Before the first public post or artwork, confirm that contributor credit and publishing context are correct.' },
|
||||
]
|
||||
|
||||
export const STUDIO_AREAS = [
|
||||
'Dashboard for a quick read on releases, review items, events, and activity.',
|
||||
'Artworks for group-owned publishing and shared presentation.',
|
||||
'Review Queue for approval workflows, changes requested, and moderation hygiene.',
|
||||
'Posts for announcements, release notes, recruitment, and milestones.',
|
||||
'Projects for structured collaboration and shared progress.',
|
||||
'Challenges and Events for community activity and time-based launches.',
|
||||
'Assets for internal shared files and reusable resources.',
|
||||
'Collections and Releases for public packaging, curation, and major launch moments.',
|
||||
'Members, Recruitment, Invitations, and Reputation for team health and trust signals.',
|
||||
]
|
||||
|
||||
export const FEATURE_CARDS = [
|
||||
{
|
||||
title: 'Projects',
|
||||
body: 'Use projects when a collaboration needs a shared home, milestones, attachments, and team ownership before it becomes a public release.',
|
||||
},
|
||||
{
|
||||
title: 'Challenges',
|
||||
body: 'Challenges work well for themed prompts, community events, and structured contribution windows that keep the Group active.',
|
||||
},
|
||||
{
|
||||
title: 'Events',
|
||||
body: 'Events are for launches, showcases, meetups, timed drops, or public moments that need a calendar-style anchor.',
|
||||
},
|
||||
{
|
||||
title: 'Assets',
|
||||
body: 'The asset library keeps shared files, references, and working materials organized instead of buried in chat history.',
|
||||
},
|
||||
{
|
||||
title: 'Releases',
|
||||
body: 'Releases package a major publication moment with summary, contributors, milestones, notes, and linked work in one polished surface.',
|
||||
},
|
||||
{
|
||||
title: 'Posts',
|
||||
body: 'Posts keep the Group human. Use them for updates, recruitment, changelogs, launch notes, and curated public communication.',
|
||||
},
|
||||
]
|
||||
|
||||
export const BEST_PRACTICES = [
|
||||
'Keep the Group identity focused. Visitors should understand who you are in seconds.',
|
||||
'Define Owner, Admin, Editor, and Contributor responsibilities early.',
|
||||
'Use the simplest permissions setup that supports the team today.',
|
||||
'Check publishing context before every public action.',
|
||||
'Credit real people accurately, even when the Group is the publish surface.',
|
||||
'Use projects and releases for bigger work instead of burying everything in posts.',
|
||||
'Keep assets tidy so new members are not onboarding into chaos.',
|
||||
'Pin only the most important public update, not every update.',
|
||||
'Review inactive memberships and old roles periodically.',
|
||||
'Use recruitment only when the Group can actually onboard people well.',
|
||||
'Write release notes that explain what changed and why it matters.',
|
||||
'Treat the Group page like a living portfolio, not a one-time setup screen.',
|
||||
]
|
||||
|
||||
export const COMMON_MISTAKES = [
|
||||
'Giving too many people admin power before the team knows how it wants to work.',
|
||||
'Publishing under the wrong context because no one checked whether Personal Studio or Group Studio was active.',
|
||||
'Forgetting contributor credit or using vague labels that do not explain the work.',
|
||||
'Creating a Group with no clear purpose, rhythm, or public identity.',
|
||||
'Letting the public page go stale after the initial setup.',
|
||||
'Using the Group identity to hide who actually made the work.',
|
||||
'Inviting members without setting expectations around ownership, publishing, and approval flow.',
|
||||
'Treating posts as noise instead of meaningful updates.',
|
||||
'Keeping an asset library that nobody can search or trust.',
|
||||
'Adding projects, challenges, events, and releases before the team has a simple baseline workflow.',
|
||||
]
|
||||
|
||||
export const WORKFLOWS = [
|
||||
{
|
||||
title: 'Workflow A: Small trusted team',
|
||||
summary: 'Owner plus one editor and one or two contributors. Simple permissions, direct publishing when the team already trusts the process.',
|
||||
bullets: ['Use lightweight posts for updates.', 'Keep contributor labels accurate.', 'Only add milestones when a release needs coordination.'],
|
||||
},
|
||||
{
|
||||
title: 'Workflow B: Growing art collective',
|
||||
summary: 'Owner, admins, editors, and contributors with a review queue and recruitment enabled.',
|
||||
bullets: ['Use projects for medium-term work.', 'Review submissions before public publishing.', 'Keep the member list and public page curated.'],
|
||||
},
|
||||
{
|
||||
title: 'Workflow C: Release-driven group',
|
||||
summary: 'A team built around themed drops, packs, or showcase moments.',
|
||||
bullets: ['Use releases as the main public storytelling surface.', 'Attach artworks, release notes, and milestones.', 'Pair launches with a post and pinned update.'],
|
||||
},
|
||||
{
|
||||
title: 'Workflow D: Community challenge group',
|
||||
summary: 'A Group centered on challenges, events, and recurring prompts.',
|
||||
bullets: ['Use join requests or recruiting to control growth.', 'Publish clear challenge briefs and event dates.', 'Keep moderation and review communication constructive.'],
|
||||
},
|
||||
]
|
||||
|
||||
export const FAQ_ITEMS = [
|
||||
{
|
||||
question: 'What is the difference between a personal profile and a Group?',
|
||||
answer: 'A personal profile is your individual identity. A Group is a shared identity for collaboration and publishing. The Group can publish the work publicly, but contributor credit should still show the people behind it.',
|
||||
},
|
||||
{
|
||||
question: 'Can I publish both personally and as a Group?',
|
||||
answer: 'Yes. Many creators keep their personal portfolio active while also publishing collaborative work under a Group. The key is to choose the right context before you publish.',
|
||||
},
|
||||
{
|
||||
question: 'Will my name still appear if I publish under a Group?',
|
||||
answer: 'Yes. Group publishing is meant to preserve authorship, not erase it. Uploaded by, primary author, and contributor credit should still reflect the humans involved.',
|
||||
},
|
||||
{
|
||||
question: 'Who can publish as a Group?',
|
||||
answer: 'That depends on role and workflow. Owners and admins usually can. Editors often can. Contributors may submit drafts without publishing directly if the Group uses review-first workflows.',
|
||||
},
|
||||
{
|
||||
question: 'Can contributors submit drafts without publishing directly?',
|
||||
answer: 'Yes. That is one of the most useful Group workflows. Contributors can hand work off for review while editors or admins handle final approval and public publishing.',
|
||||
},
|
||||
{
|
||||
question: 'How do I invite members?',
|
||||
answer: 'Open Group Studio, go to members or invitations, choose the right role, and send an invite only after you know what that person needs access to.',
|
||||
},
|
||||
{
|
||||
question: 'Can I remove a member later?',
|
||||
answer: 'Yes. Owners and people with the right member-management permissions can update roles, revoke invitations, or remove active members when needed.',
|
||||
},
|
||||
{
|
||||
question: 'What happens to old artworks if someone leaves the Group?',
|
||||
answer: 'Existing publishing history and contributor credit should remain as part of the public record unless the content itself changes or is removed through normal moderation or management flows.',
|
||||
},
|
||||
{
|
||||
question: 'Should every team create a Group?',
|
||||
answer: 'No. Create a Group when you need shared identity, shared workflows, or recurring collaboration. If you only publish solo work, a personal profile may be enough.',
|
||||
},
|
||||
{
|
||||
question: 'How should we use roles?',
|
||||
answer: 'Start simple. Keep Owner count small, reserve Admin for deeply trusted operators, use Editor for day-to-day management, and keep Contributor focused on creative participation.',
|
||||
},
|
||||
{
|
||||
question: 'Can we recruit new members through the Group page?',
|
||||
answer: 'Yes, if recruitment is enabled. Use it when the Group is actually ready to onboard people, not just to look active.',
|
||||
},
|
||||
{
|
||||
question: 'What are releases, projects, and challenges used for?',
|
||||
answer: 'Projects help teams organize work. Releases package major publication moments. Challenges create themed participation and energy. Together they make the Group feel structured and alive.',
|
||||
},
|
||||
{
|
||||
question: 'Can a Group have posts and announcements?',
|
||||
answer: 'Yes. Posts are useful for release notes, milestone updates, recruitment, public announcements, and pinned context on the Group page.',
|
||||
},
|
||||
{
|
||||
question: 'How should we organize our assets?',
|
||||
answer: 'Keep them categorized, named clearly, and cleaned up over time. Shared assets should help the team work faster, not become a second mystery archive.',
|
||||
},
|
||||
{
|
||||
question: 'What should we do if contributor credit is wrong?',
|
||||
answer: 'Fix it quickly. Attribution errors create confusion, trust issues, and sometimes conflict. Confirm who uploaded the work, who authored it, and who contributed before changing anything publicly.',
|
||||
},
|
||||
]
|
||||
|
||||
export const TROUBLESHOOTING_ITEMS = [
|
||||
{
|
||||
title: 'I cannot publish as the Group',
|
||||
body: 'Check that you are in Group Studio, not Personal Studio. Then confirm your role allows publishing and that the Group is active and not archived or suspended.',
|
||||
},
|
||||
{
|
||||
title: 'I do not see Group Studio',
|
||||
body: 'You may not be signed in, may not belong to the Group, or may only have a pending invitation. Accept the invite first, then reload Studio.',
|
||||
},
|
||||
{
|
||||
title: 'My role does not let me do what I expected',
|
||||
body: 'Ask the Owner or Admin which permissions are meant for your role. In many teams, Editors manage content while Contributors only submit drafts.',
|
||||
},
|
||||
{
|
||||
title: 'Contributor credit is wrong',
|
||||
body: 'Review the publish record carefully: published by, uploaded by, primary author, and contributors each mean different things. Correct the one that is inaccurate rather than replacing all of them.',
|
||||
},
|
||||
{
|
||||
title: 'I was invited but cannot access content',
|
||||
body: 'Some areas may still be internal, role-limited, or pending approval. First confirm that the invitation was accepted and the membership is active.',
|
||||
},
|
||||
{
|
||||
title: 'I published under the wrong context',
|
||||
body: 'Stop and review the affected content immediately. Confirm whether it should live under the personal profile or the Group, then correct the publish context before more linked items are built around it.',
|
||||
},
|
||||
{
|
||||
title: 'I do not understand why my draft is in review',
|
||||
body: 'Your Group may use a review-first workflow so contributors can submit work without publishing directly. Check the review queue feedback or ask the assigned reviewer what needs to change.',
|
||||
},
|
||||
{
|
||||
title: 'I do not know which role to assign someone',
|
||||
body: 'Ask what they need to do this month, not what title sounds impressive. If you are unsure, start lower and promote later.',
|
||||
},
|
||||
]
|
||||
160
resources/js/Pages/Group/groupQuickstartContent.js
Normal file
160
resources/js/Pages/Group/groupQuickstartContent.js
Normal file
@@ -0,0 +1,160 @@
|
||||
export const SECTION_ITEMS = [
|
||||
{ id: 'introduction', label: 'Welcome' },
|
||||
{ id: 'what-is-a-group', label: 'What is a Group?' },
|
||||
{ id: 'when-to-use', label: 'When to use a Group' },
|
||||
{ id: 'create-first-group', label: 'Create your first Group' },
|
||||
{ id: 'setup-properly', label: 'Set it up properly' },
|
||||
{ id: 'invite-and-roles', label: 'Invite members and roles' },
|
||||
{ id: 'publish-first-artwork', label: 'Publish your first artwork' },
|
||||
{ id: 'contributor-credit', label: 'Contributor credit' },
|
||||
{ id: 'first-week-best-practices', label: 'First-week best practices' },
|
||||
{ id: 'common-mistakes', label: 'Common mistakes' },
|
||||
{ id: 'quick-checklist', label: 'Quick checklist' },
|
||||
{ id: 'next-steps', label: 'Next steps' },
|
||||
]
|
||||
|
||||
export const COMPARISON_CARDS = [
|
||||
{
|
||||
title: 'Personal profile',
|
||||
icon: 'fa-solid fa-user',
|
||||
bullets: ['Individual identity', 'Solo publishing', 'Personal portfolio and reputation'],
|
||||
},
|
||||
{
|
||||
title: 'Group',
|
||||
icon: 'fa-solid fa-people-group',
|
||||
bullets: ['Team identity', 'Collaborative publishing', 'Shared brand, shared activity, shared workflow'],
|
||||
},
|
||||
]
|
||||
|
||||
export const GOOD_FIT = [
|
||||
'You work with other creators regularly.',
|
||||
'You want a shared public brand for a studio, team, or collective.',
|
||||
'You want one home for member roles, publishing, and shared activity.',
|
||||
'You release projects, themed drops, or collaborative packs together.',
|
||||
]
|
||||
|
||||
export const NOT_NEEDED_YET = [
|
||||
'You only publish solo work right now.',
|
||||
'You do not need shared identity or member management yet.',
|
||||
'You are not collaborating enough to justify shared workflow overhead.',
|
||||
]
|
||||
|
||||
export const CREATE_STEPS = [
|
||||
{ title: 'Open Groups or Creator Studio', description: 'Start from the Groups area in Studio and choose Create Group.' },
|
||||
{ title: 'Choose your name and slug', description: 'Pick something clear, memorable, and easy for other creators to recognize.' },
|
||||
{ title: 'Add your visuals', description: 'Upload a logo or avatar and a cover image so the Group feels real immediately.' },
|
||||
{ title: 'Write a short description', description: 'Explain what the Group makes and who it is for in one strong paragraph.' },
|
||||
{ title: 'Choose visibility', description: 'Decide whether the Group should be public, unlisted, or private while you set it up.' },
|
||||
{ title: 'Create the Group', description: 'Finish creation, then review the public page before you invite the rest of the team.' },
|
||||
]
|
||||
|
||||
export const SETUP_TASKS = [
|
||||
'Upload a clean avatar or logo.',
|
||||
'Add a cover image that matches the Group identity.',
|
||||
'Write a short description instead of leaving the page blank.',
|
||||
'Decide who should be Owner and who really needs Admin access.',
|
||||
'Choose public or private visibility intentionally.',
|
||||
'Make the page feel alive before you ask people to join it.',
|
||||
]
|
||||
|
||||
export const ROLE_CARDS = [
|
||||
{
|
||||
role: 'Owner',
|
||||
summary: 'Full control over branding, membership, settings, and the overall workflow.',
|
||||
note: 'Keep this count very small.',
|
||||
},
|
||||
{
|
||||
role: 'Admin',
|
||||
summary: 'Helps run the Group day to day, manage members, and keep operations moving.',
|
||||
note: 'Only give this to deeply trusted people.',
|
||||
},
|
||||
{
|
||||
role: 'Editor',
|
||||
summary: 'A strong fit for content managers, release coordinators, and people who help publish work.',
|
||||
note: 'Usually the best default for trusted operators.',
|
||||
},
|
||||
{
|
||||
role: 'Contributor',
|
||||
summary: 'Contributes work without needing full control over the Group structure.',
|
||||
note: 'Best starting role for most collaborators.',
|
||||
},
|
||||
]
|
||||
|
||||
export const PUBLISH_STEPS = [
|
||||
{ title: 'Open Group Studio', description: 'Make sure you are working inside the Group, not your personal publishing context.' },
|
||||
{ title: 'Start the upload or open the draft', description: 'Prepare the artwork that should appear under the Group identity publicly.' },
|
||||
{ title: 'Confirm Group context before publish', description: 'Double-check that you are publishing as the Group, not as your personal profile.' },
|
||||
{ title: 'Review credit before final publish', description: 'Check primary author and contributor fields before the artwork goes public.' },
|
||||
]
|
||||
|
||||
export const CREDIT_TERMS = [
|
||||
{ label: 'Published by', value: 'Warlock', note: 'The shared identity the artwork appears under publicly.' },
|
||||
{ label: 'Uploaded by', value: 'Gregor', note: 'The person who performed the upload or final publish action.' },
|
||||
{ label: 'Primary author', value: 'Gregor', note: 'The main author of the work.' },
|
||||
{ label: 'Contributors', value: 'Denis, Paula', note: 'Additional people who made meaningful creative contributions.' },
|
||||
]
|
||||
|
||||
export const FIRST_WEEK_BEST_PRACTICES = [
|
||||
'Publish one strong piece before publishing a lot of weak or unfinished work.',
|
||||
'Fill out the Group profile early so the public page does not feel abandoned.',
|
||||
'Agree internally on how contributor credit should be assigned before launch day.',
|
||||
'Keep roles simple until the team actually needs more complexity.',
|
||||
'Use posts and updates for meaningful announcements, not noise.',
|
||||
'Feature the best work so the Group makes a strong first impression.',
|
||||
]
|
||||
|
||||
export const COMMON_MISTAKES = [
|
||||
'Giving too many people admin access too early.',
|
||||
'Publishing under the wrong context because no one checked whether the Group was selected.',
|
||||
'Forgetting contributor credit or leaving it vague.',
|
||||
'Creating a Group with no real purpose or activity plan.',
|
||||
'Leaving the profile blank and expecting the page to feel trustworthy.',
|
||||
'Overcomplicating permissions on day one.',
|
||||
'Letting inactive members keep strong permissions forever.',
|
||||
'Using the Group identity without clear authorship inside the team.',
|
||||
]
|
||||
|
||||
export const QUICK_CHECKLIST = [
|
||||
'Group created',
|
||||
'Name and slug chosen',
|
||||
'Avatar or logo uploaded',
|
||||
'Cover added',
|
||||
'Description written',
|
||||
'First members invited',
|
||||
'Roles assigned',
|
||||
'Group context selected in Studio',
|
||||
'First artwork prepared',
|
||||
'Contributor credit reviewed',
|
||||
'First Group publish completed',
|
||||
]
|
||||
|
||||
export const NEXT_STEPS = [
|
||||
{
|
||||
eyebrow: 'Create',
|
||||
title: 'Create a Group',
|
||||
body: 'Start the shared identity now if you are ready to move from solo work to team publishing.',
|
||||
linkKey: 'create_group',
|
||||
tone: 'sky',
|
||||
},
|
||||
{
|
||||
eyebrow: 'Manage',
|
||||
title: 'Open Group Studio',
|
||||
body: 'Go straight into Studio if your Group already exists and you want to invite members or publish.',
|
||||
linkKey: 'group_studio',
|
||||
tone: 'white',
|
||||
},
|
||||
{
|
||||
eyebrow: 'Learn more',
|
||||
title: 'Read the full Groups guide',
|
||||
body: 'Open the deeper documentation for releases, challenges, review workflows, troubleshooting, and advanced usage.',
|
||||
linkKey: 'full_documentation',
|
||||
tone: 'amber',
|
||||
},
|
||||
{
|
||||
eyebrow: 'Explore',
|
||||
title: 'Browse public Groups',
|
||||
body: 'See how other teams present themselves, structure their identity, and publish together.',
|
||||
linkKey: 'groups_directory',
|
||||
tone: 'white',
|
||||
},
|
||||
]
|
||||
Reference in New Issue
Block a user