Commit workspace changes
This commit is contained in:
12
resources/js/components/groups/GroupBadgePill.jsx
Normal file
12
resources/js/components/groups/GroupBadgePill.jsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react'
|
||||
import { toneClasses } from './groupStyles'
|
||||
|
||||
export default function GroupBadgePill({ label, tone = 'slate', className = '' }) {
|
||||
if (!label) return null
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center rounded-full border px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] ${toneClasses(tone)} ${className}`.trim()}>
|
||||
{label}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
19
resources/js/components/groups/GroupBrowseFilters.jsx
Normal file
19
resources/js/components/groups/GroupBrowseFilters.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function GroupBrowseFilters({ surfaces = [], currentSurface = 'featured' }) {
|
||||
if (!Array.isArray(surfaces) || surfaces.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="mt-6 flex flex-wrap gap-2">
|
||||
{surfaces.map((surface) => (
|
||||
<a
|
||||
key={surface.value}
|
||||
href={`/groups?surface=${encodeURIComponent(surface.value)}`}
|
||||
className={`rounded-full border px-4 py-2 text-sm font-semibold transition ${currentSurface === surface.value ? 'border-sky-300/25 bg-sky-300/10 text-sky-100' : 'border-white/10 bg-white/[0.03] text-white hover:border-white/20 hover:bg-white/[0.06]'}`}
|
||||
>
|
||||
{surface.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
82
resources/js/components/groups/GroupDiscoveryCard.jsx
Normal file
82
resources/js/components/groups/GroupDiscoveryCard.jsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React from 'react'
|
||||
import GroupBadgePill from './GroupBadgePill'
|
||||
import { cx, formatCompactNumber } from './groupStyles'
|
||||
|
||||
export default function GroupDiscoveryCard({ group, className = '', compact = false }) {
|
||||
if (!group) return null
|
||||
|
||||
const primarySummary = group.headline || group.bio_excerpt || 'Collaborative publishing identity on Skinbase Nova.'
|
||||
|
||||
return (
|
||||
<a
|
||||
href={group.urls?.public || '/groups'}
|
||||
className={cx(
|
||||
'group block overflow-hidden rounded-[30px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-5 shadow-[0_24px_70px_rgba(2,6,23,0.34)] transition duration-200 hover:-translate-y-1 hover:border-white/20',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex h-14 w-14 shrink-0 items-center justify-center overflow-hidden rounded-2xl border border-white/10 bg-white/[0.04]">
|
||||
{group.avatar_url ? (
|
||||
<img src={group.avatar_url} alt={group.name} className="h-full w-full object-cover" loading="lazy" />
|
||||
) : (
|
||||
<i className="fa-solid fa-people-group text-slate-300" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h3 className="truncate text-lg font-semibold text-white">{group.name}</h3>
|
||||
{group.is_recruiting ? <GroupBadgePill label="Recruiting" tone="emerald" /> : null}
|
||||
{group.is_verified ? <GroupBadgePill label="Verified" tone="sky" /> : null}
|
||||
</div>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-300">{primarySummary}</p>
|
||||
{group.owner?.username || group.owner?.name ? (
|
||||
<p className="mt-2 text-xs uppercase tracking-[0.16em] text-slate-500">
|
||||
Led by {group.owner?.username || group.owner?.name}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
{(Array.isArray(group.trust_signals) ? group.trust_signals : []).slice(0, compact ? 2 : 3).map((signal) => (
|
||||
<GroupBadgePill key={signal.key} label={signal.label} tone={signal.tone} />
|
||||
))}
|
||||
{(Array.isArray(group.badges) ? group.badges : []).slice(0, compact ? 1 : 2).map((badge) => (
|
||||
<GroupBadgePill key={badge.key} label={badge.label} tone="amber" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{group.recruitment_headline && !compact ? (
|
||||
<div className="mt-5 rounded-2xl border border-emerald-300/20 bg-emerald-400/10 px-4 py-3 text-sm text-emerald-50">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-emerald-200/80">Open call</div>
|
||||
<div className="mt-1">{group.recruitment_headline}</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{group.featured_release?.title && !compact ? (
|
||||
<div className="mt-5 rounded-2xl border border-white/10 bg-black/20 p-4">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-amber-100/80">Featured release</div>
|
||||
<div className="mt-2 text-base font-semibold text-white">{group.featured_release.title}</div>
|
||||
{group.featured_release.summary ? <div className="mt-1 text-sm text-slate-400">{group.featured_release.summary}</div> : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-5 grid grid-cols-3 gap-2 rounded-2xl border border-white/10 bg-white/[0.03] p-3 text-center">
|
||||
<div>
|
||||
<div className="text-lg font-semibold text-white">{formatCompactNumber(group.counts?.artworks)}</div>
|
||||
<div className="text-[11px] uppercase tracking-[0.16em] text-slate-500">Artworks</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg font-semibold text-white">{formatCompactNumber(group.counts?.members)}</div>
|
||||
<div className="text-[11px] uppercase tracking-[0.16em] text-slate-500">Members</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg font-semibold text-white">{formatCompactNumber(group.counts?.followers)}</div>
|
||||
<div className="text-[11px] uppercase tracking-[0.16em] text-slate-500">Followers</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
43
resources/js/components/groups/GroupLeaderboardCard.jsx
Normal file
43
resources/js/components/groups/GroupLeaderboardCard.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react'
|
||||
import GroupBadgePill from './GroupBadgePill'
|
||||
|
||||
export default function GroupLeaderboardCard({ item }) {
|
||||
if (!item?.entity) return null
|
||||
|
||||
const entity = item.entity
|
||||
|
||||
return (
|
||||
<article className="rounded-[26px] border border-white/10 bg-white/[0.03] p-4 shadow-[0_18px_50px_rgba(2,6,23,0.3)]">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl border border-white/10 bg-slate-950/70 text-lg font-black text-white">
|
||||
#{item.rank}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<a href={entity.url || '/groups'} className="block truncate text-lg font-semibold text-white transition hover:text-sky-300">{entity.name}</a>
|
||||
{entity.headline ? <p className="mt-1 text-sm text-slate-400">{entity.headline}</p> : null}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-[11px] uppercase tracking-[0.18em] text-slate-500">Score</div>
|
||||
<div className="mt-1 text-xl font-black text-white">{Number(item.score || 0).toLocaleString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{(Array.isArray(entity.trust_signals) ? entity.trust_signals : []).slice(0, 2).map((signal) => (
|
||||
<GroupBadgePill key={signal.key} label={signal.label} tone={signal.tone} />
|
||||
))}
|
||||
{entity.is_recruiting ? <GroupBadgePill label="Recruiting" tone="emerald" /> : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-4 text-xs text-slate-400">
|
||||
<span>{Number(entity.artworks_count || 0).toLocaleString()} artworks</span>
|
||||
<span>{Number(entity.members_count || 0).toLocaleString()} members</span>
|
||||
<span>{Number(entity.followers_count || 0).toLocaleString()} followers</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
51
resources/js/components/groups/GroupProfileSummary.jsx
Normal file
51
resources/js/components/groups/GroupProfileSummary.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from 'react'
|
||||
import GroupBadgePill from './GroupBadgePill'
|
||||
|
||||
export default function GroupProfileSummary({ contributions = [], href = null }) {
|
||||
if (!Array.isArray(contributions) || contributions.length === 0) return null
|
||||
|
||||
const featured = contributions.slice(0, 3)
|
||||
|
||||
return (
|
||||
<section className="mx-auto mt-8 max-w-6xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="rounded-[30px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(14,165,233,0.12),transparent_34%),linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))] p-5 shadow-[0_18px_55px_rgba(2,6,23,0.28)] backdrop-blur-xl sm:p-6">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80">Group footprint</div>
|
||||
<h2 className="mt-2 text-2xl font-semibold text-white">Collaborative work across public groups</h2>
|
||||
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-400">See the groups this creator contributes to through releases, credited artworks, and shared publishing activity.</p>
|
||||
</div>
|
||||
{href ? <a href={href} className="text-sm font-semibold text-sky-200 transition hover:text-white">View full contribution history</a> : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid gap-4 lg:grid-cols-3">
|
||||
{featured.map((entry) => (
|
||||
<a key={entry.group?.slug || entry.group?.id} href={entry.group?.profile_url || '/groups'} className="rounded-[24px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20 hover:bg-black/30">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-2xl border border-white/10 bg-white/[0.04]">
|
||||
{entry.group?.avatar_url ? <img src={entry.group.avatar_url} alt={entry.group?.name} className="h-full w-full object-cover" loading="lazy" /> : <i className="fa-solid fa-people-group text-slate-300" />}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-base font-semibold text-white">{entry.group?.name}</div>
|
||||
{entry.role?.label ? <div className="mt-1 text-sm text-slate-400">{entry.role.label}</div> : null}
|
||||
{entry.summary ? <div className="mt-2 text-sm text-slate-300">{entry.summary}</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{entry.trusted ? <GroupBadgePill label="Trusted contributor" tone="sky" /> : null}
|
||||
{entry.recent_release_titles?.length ? <GroupBadgePill label="Recent releases" tone="amber" /> : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-4 text-xs text-slate-400">
|
||||
<span>{Number(entry.counts?.artworks || 0).toLocaleString()} artworks</span>
|
||||
<span>{Number(entry.counts?.releases || 0).toLocaleString()} releases</span>
|
||||
<span>{Number(entry.counts?.projects || 0).toLocaleString()} projects</span>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
66
resources/js/components/groups/GroupPromoCard.jsx
Normal file
66
resources/js/components/groups/GroupPromoCard.jsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React from 'react'
|
||||
import GroupBadgePill from './GroupBadgePill'
|
||||
|
||||
export default function GroupPromoCard({ group, eyebrow = 'Groups spotlight', title, description, ctaLabel = 'Open group' }) {
|
||||
if (!group) return null
|
||||
|
||||
return (
|
||||
<section className="overflow-hidden rounded-[34px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.18),transparent_28%),radial-gradient(circle_at_80%_20%,rgba(16,185,129,0.12),transparent_26%),linear-gradient(180deg,rgba(7,16,29,0.98),rgba(2,6,23,0.94))] shadow-[0_30px_90px_rgba(2,6,23,0.45)]">
|
||||
<div className="grid gap-6 p-6 lg:grid-cols-[minmax(0,1.3fr)_320px] lg:p-8">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.28em] text-sky-200/80">{eyebrow}</p>
|
||||
<h2 className="mt-3 text-3xl font-semibold tracking-[-0.03em] text-white sm:text-4xl">{title || group.name}</h2>
|
||||
<p className="mt-4 max-w-2xl text-sm leading-7 text-slate-300">{description || group.headline || group.bio_excerpt || 'Collective publishing for shared releases, artwork credits, and collaborative identity.'}</p>
|
||||
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
{(Array.isArray(group.trust_signals) ? group.trust_signals : []).slice(0, 3).map((signal) => (
|
||||
<GroupBadgePill key={signal.key} label={signal.label} tone={signal.tone} />
|
||||
))}
|
||||
{group.is_recruiting ? <GroupBadgePill label="Actively recruiting" tone="emerald" /> : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-wrap gap-3">
|
||||
<a href={group.urls?.public || '/groups'} className="inline-flex items-center gap-2 rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-2 text-sm font-semibold text-sky-100 transition hover:border-sky-300/40 hover:bg-sky-300/15">
|
||||
{ctaLabel}
|
||||
<i className="fa-solid fa-arrow-right text-xs" />
|
||||
</a>
|
||||
<a href="/groups" className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.07]">
|
||||
Browse all groups
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[28px] border border-white/10 bg-black/25 p-5 backdrop-blur-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex h-16 w-16 shrink-0 items-center justify-center overflow-hidden rounded-2xl border border-white/10 bg-white/[0.04]">
|
||||
{group.avatar_url ? <img src={group.avatar_url} alt={group.name} className="h-full w-full object-cover" loading="lazy" /> : <i className="fa-solid fa-people-group text-slate-300" />}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-lg font-semibold text-white">{group.name}</div>
|
||||
<div className="mt-1 text-sm text-slate-400">{group.owner?.username || group.owner?.name ? `Led by ${group.owner.username || group.owner.name}` : 'Shared publishing identity'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid grid-cols-2 gap-3">
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-3">
|
||||
<div className="text-2xl font-semibold text-white">{Number(group.counts?.artworks || 0).toLocaleString()}</div>
|
||||
<div className="text-[11px] uppercase tracking-[0.18em] text-slate-500">Published artworks</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-3">
|
||||
<div className="text-2xl font-semibold text-white">{Number(group.counts?.followers || 0).toLocaleString()}</div>
|
||||
<div className="text-[11px] uppercase tracking-[0.18em] text-slate-500">Followers</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-3">
|
||||
<div className="text-2xl font-semibold text-white">{Number(group.counts?.members || 0).toLocaleString()}</div>
|
||||
<div className="text-[11px] uppercase tracking-[0.18em] text-slate-500">Members</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-3">
|
||||
<div className="text-2xl font-semibold text-white">{Number(group.counts?.collections || 0).toLocaleString()}</div>
|
||||
<div className="text-[11px] uppercase tracking-[0.18em] text-slate-500">Collections</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
27
resources/js/components/groups/GroupStudioPromoCard.jsx
Normal file
27
resources/js/components/groups/GroupStudioPromoCard.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function GroupStudioPromoCard({ title, description, bullets = [], primaryLabel, primaryHref, secondaryLabel, secondaryHref }) {
|
||||
return (
|
||||
<section className="rounded-[30px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_34%),linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))] p-6 shadow-[0_18px_55px_rgba(2,6,23,0.28)]">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80">Studio groups</div>
|
||||
<h2 className="mt-2 text-2xl font-semibold text-white">{title}</h2>
|
||||
<p className="mt-3 max-w-2xl text-sm leading-6 text-slate-300">{description}</p>
|
||||
|
||||
{bullets.length > 0 ? (
|
||||
<div className="mt-5 grid gap-3 md:grid-cols-3">
|
||||
{bullets.map((bullet) => (
|
||||
<div key={bullet.title} className="rounded-[22px] border border-white/10 bg-black/20 p-4">
|
||||
<div className="text-sm font-semibold text-white">{bullet.title}</div>
|
||||
<div className="mt-2 text-sm leading-6 text-slate-400">{bullet.body}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-5 flex flex-wrap gap-3">
|
||||
{primaryHref ? <a href={primaryHref} className="rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-2 text-sm font-semibold text-sky-100 transition hover:border-sky-300/35 hover:bg-sky-300/15">{primaryLabel}</a> : null}
|
||||
{secondaryHref ? <a href={secondaryHref} className="rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.07]">{secondaryLabel}</a> : null}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
48
resources/js/components/groups/GroupSummaryPanel.jsx
Normal file
48
resources/js/components/groups/GroupSummaryPanel.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react'
|
||||
import GroupBadgePill from './GroupBadgePill'
|
||||
|
||||
export default function GroupSummaryPanel({ group, artwork }) {
|
||||
if (!group) return null
|
||||
|
||||
return (
|
||||
<section className="overflow-hidden rounded-[28px] border border-white/[0.08] bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_36%),linear-gradient(180deg,rgba(255,255,255,0.06),rgba(255,255,255,0.02))] px-5 py-5 shadow-[0_22px_55px_rgba(0,0,0,0.26)] backdrop-blur-xl sm:px-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<a href={group.urls?.public || '/groups'} className="flex h-16 w-16 shrink-0 items-center justify-center overflow-hidden rounded-2xl border border-white/10 bg-white/[0.04]">
|
||||
{group.avatar_url ? <img src={group.avatar_url} alt={group.name} className="h-full w-full object-cover" loading="lazy" /> : <i className="fa-solid fa-people-group text-slate-300" />}
|
||||
</a>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80">Published as a group</div>
|
||||
<a href={group.urls?.public || '/groups'} className="mt-2 block text-xl font-semibold tracking-[-0.02em] text-white transition hover:text-sky-300">{group.name}</a>
|
||||
<p className="mt-2 text-sm leading-6 text-white/65">{group.headline || group.bio_excerpt || `${artwork?.title || 'This artwork'} is credited to a collaborative group identity on Skinbase.`}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{(Array.isArray(group.trust_signals) ? group.trust_signals : []).slice(0, 3).map((signal) => (
|
||||
<GroupBadgePill key={signal.key} label={signal.label} tone={signal.tone} />
|
||||
))}
|
||||
{group.is_recruiting ? <GroupBadgePill label="Recruiting" tone="emerald" /> : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid grid-cols-3 gap-2 rounded-2xl border border-white/10 bg-black/20 p-3 text-center">
|
||||
<div>
|
||||
<div className="text-lg font-semibold text-white">{Number(group.counts?.artworks || 0).toLocaleString()}</div>
|
||||
<div className="text-[11px] uppercase tracking-[0.16em] text-slate-500">Artworks</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg font-semibold text-white">{Number(group.counts?.members || 0).toLocaleString()}</div>
|
||||
<div className="text-[11px] uppercase tracking-[0.16em] text-slate-500">Members</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg font-semibold text-white">{Number(group.counts?.followers || 0).toLocaleString()}</div>
|
||||
<div className="text-[11px] uppercase tracking-[0.16em] text-slate-500">Followers</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
<a href={group.urls?.public || '/groups'} className="rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-2 text-sm font-semibold text-sky-100 transition hover:border-sky-300/35 hover:bg-sky-300/15">Open group page</a>
|
||||
{group.urls?.releases ? <a href={group.urls.releases} className="rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.07]">Browse releases</a> : null}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
24
resources/js/components/groups/GroupTrendingSection.jsx
Normal file
24
resources/js/components/groups/GroupTrendingSection.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react'
|
||||
import GroupDiscoveryCard from './GroupDiscoveryCard'
|
||||
|
||||
export default function GroupTrendingSection({ title, description, items = [], href = '/groups', actionLabel = 'See more' }) {
|
||||
if (!Array.isArray(items) || items.length === 0) return null
|
||||
|
||||
return (
|
||||
<section className="mt-10">
|
||||
<div className="mb-5 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-[-0.02em] text-white">{title}</h2>
|
||||
{description ? <p className="mt-2 max-w-3xl text-sm leading-6 text-slate-400">{description}</p> : null}
|
||||
</div>
|
||||
<a href={href} className="inline-flex items-center gap-2 text-sm font-semibold text-sky-200 transition hover:text-white">
|
||||
{actionLabel}
|
||||
<i className="fa-solid fa-arrow-right text-xs" />
|
||||
</a>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
{items.map((group) => <GroupDiscoveryCard key={group.slug || group.id} group={group} compact />)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
19
resources/js/components/groups/groupStyles.js
Normal file
19
resources/js/components/groups/groupStyles.js
Normal file
@@ -0,0 +1,19 @@
|
||||
export function cx(...parts) {
|
||||
return parts.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
export function formatCompactNumber(value) {
|
||||
return new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 1 }).format(Number(value || 0))
|
||||
}
|
||||
|
||||
const TONE_CLASSES = {
|
||||
sky: 'border-sky-300/25 bg-sky-300/10 text-sky-100',
|
||||
emerald: 'border-emerald-300/25 bg-emerald-300/10 text-emerald-100',
|
||||
amber: 'border-amber-300/25 bg-amber-300/10 text-amber-100',
|
||||
violet: 'border-violet-300/25 bg-violet-300/10 text-violet-100',
|
||||
slate: 'border-white/10 bg-white/[0.04] text-slate-200',
|
||||
}
|
||||
|
||||
export function toneClasses(tone = 'slate') {
|
||||
return TONE_CLASSES[tone] || TONE_CLASSES.slate
|
||||
}
|
||||
Reference in New Issue
Block a user