Commit workspace changes
This commit is contained in:
@@ -7,121 +7,49 @@ function normalizeItems(items) {
|
||||
return items.filter((item) => item && typeof item === 'object')
|
||||
}
|
||||
|
||||
function SectionHeader({ title, subtitle, href, ctaLabel = 'See all' }) {
|
||||
return (
|
||||
<div className="mb-5 flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-white">{title}</h3>
|
||||
{subtitle ? <p className="mt-1 text-xs text-nova-400">{subtitle}</p> : null}
|
||||
</div>
|
||||
{href ? (
|
||||
<a href={href} className="shrink-0 text-sm text-nova-300 transition hover:text-white">
|
||||
{ctaLabel} →
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CollectionStrip({ items }) {
|
||||
if (!items.length) return null
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{items.map((collection) => (
|
||||
<CollectionCard key={collection.id} collection={collection} isOwner={false} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CollectionSection({ title, subtitle, href, items, limit = 3, ctaLabel }) {
|
||||
const normalized = normalizeItems(items).slice(0, limit)
|
||||
if (!normalized.length) return null
|
||||
|
||||
return (
|
||||
<section className="mt-10">
|
||||
<SectionHeader title={title} subtitle={subtitle} href={href} ctaLabel={ctaLabel} />
|
||||
<CollectionStrip items={normalized} />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default function HomeCollections({
|
||||
featured,
|
||||
recent,
|
||||
trending,
|
||||
editorial,
|
||||
community,
|
||||
isLoggedIn = false,
|
||||
}) {
|
||||
const featuredItems = normalizeItems(featured)
|
||||
const recentItems = normalizeItems(recent)
|
||||
const trendingItems = normalizeItems(trending)
|
||||
const editorialItems = normalizeItems(editorial)
|
||||
const communityItems = normalizeItems(community)
|
||||
const displayItems = (
|
||||
trendingItems.length ? trendingItems :
|
||||
featuredItems.length ? featuredItems :
|
||||
recentItems.length ? recentItems :
|
||||
editorialItems.length ? editorialItems :
|
||||
communityItems
|
||||
).slice(0, 3)
|
||||
|
||||
if (!featuredItems.length && !recentItems.length && !trendingItems.length && !editorialItems.length && !communityItems.length) {
|
||||
if (!displayItems.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="mt-14 px-4 sm:px-6 lg:px-8">
|
||||
<div className="mb-6 flex flex-wrap items-end justify-between gap-4">
|
||||
<div className="mb-5 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white">Curated Collections</h2>
|
||||
<h2 className="text-xl font-bold text-white">Trending Collections</h2>
|
||||
<p className="mt-1 max-w-2xl text-sm text-nova-300">
|
||||
Hand-built galleries, smart collections, and community showcases worth opening next.
|
||||
Collections getting the strongest mix of follows, saves, and engagement right now.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 text-xs font-semibold uppercase tracking-[0.16em] text-nova-400">
|
||||
{isLoggedIn && recentItems.length ? <span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1">Recent</span> : null}
|
||||
{featuredItems.length ? <span className="rounded-full border border-amber-300/20 bg-amber-300/10 px-3 py-1 text-amber-100">Featured</span> : null}
|
||||
{communityItems.length ? <span className="rounded-full border border-sky-300/20 bg-sky-400/10 px-3 py-1 text-sky-100">Community</span> : null}
|
||||
</div>
|
||||
<a href="/collections/trending" className="shrink-0 text-sm text-nova-300 transition hover:text-white">
|
||||
All collections →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<CollectionSection
|
||||
title="Featured Collections"
|
||||
subtitle="Standout galleries with strong sequencing, presentation, or curator voice."
|
||||
href="/collections/featured"
|
||||
items={featuredItems}
|
||||
limit={3}
|
||||
/>
|
||||
|
||||
{isLoggedIn ? (
|
||||
<CollectionSection
|
||||
title="Recently Active"
|
||||
subtitle="Fresh collection activity from around the site, including new updates and resurfacing galleries."
|
||||
href="/collections/trending"
|
||||
items={recentItems}
|
||||
limit={3}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<CollectionSection
|
||||
title="Trending Collections"
|
||||
subtitle="Collections getting the strongest mix of follows, saves, and engagement right now."
|
||||
href="/collections/trending"
|
||||
items={trendingItems}
|
||||
limit={3}
|
||||
/>
|
||||
|
||||
<CollectionSection
|
||||
title="Editorial Picks"
|
||||
subtitle="Staff and premium editorial showcases with stronger themes and presentation rules."
|
||||
href="/collections/editorial"
|
||||
items={editorialItems}
|
||||
limit={3}
|
||||
/>
|
||||
|
||||
<CollectionSection
|
||||
title="Community Highlights"
|
||||
subtitle="Collaborative and submission-friendly collections that spotlight multiple creators together."
|
||||
href="/collections/community"
|
||||
items={communityItems}
|
||||
limit={3}
|
||||
/>
|
||||
<div className="grid gap-4 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{displayItems.map((collection) => (
|
||||
<CollectionCard key={collection.id} collection={collection} isOwner={false} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
107
resources/js/Pages/Home/HomeGroups.jsx
Normal file
107
resources/js/Pages/Home/HomeGroups.jsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import React from 'react'
|
||||
|
||||
function GroupSpotlightCard({ group }) {
|
||||
if (!group) return null
|
||||
|
||||
const stats = [
|
||||
{ key: 'artworks', label: 'artworks', value: Number(group.counts?.artworks || 0) },
|
||||
{ key: 'members', label: 'members', value: Number(group.counts?.members || 0) },
|
||||
{ key: 'followers', label: 'followers', value: Number(group.counts?.followers || 0) },
|
||||
].filter((item) => item.value > 0)
|
||||
|
||||
return (
|
||||
<article className="group relative flex flex-col overflow-hidden rounded-xl bg-panel p-5 shadow-sm transition hover:ring-1 hover:ring-nova-500">
|
||||
{group.banner_url ? (
|
||||
<>
|
||||
<img
|
||||
src={group.banner_url}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 h-full w-full object-cover opacity-40 transition duration-500 group-hover:scale-105 group-hover:opacity-20"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-panel via-panel/85 to-panel/70" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<a href={group.urls?.public || '/groups'} className="relative block">
|
||||
<div className="flex h-16 w-16 items-center justify-center overflow-hidden rounded-2xl bg-nova-800/80 ring-4 ring-nova-800">
|
||||
{group.avatar_url ? (
|
||||
<img
|
||||
src={group.avatar_url}
|
||||
alt={group.name}
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
) : (
|
||||
<i className="fa-solid fa-people-group text-2xl text-white" aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<h3 className="mt-3 text-base font-semibold text-white">{group.name}</h3>
|
||||
</a>
|
||||
|
||||
<p className="relative mt-2 line-clamp-3 text-sm text-soft">
|
||||
{group.headline || group.bio_excerpt || 'Shared publishing identity for collaborative releases and artwork.'}
|
||||
</p>
|
||||
|
||||
<div className="relative mt-3 flex flex-wrap gap-2 text-xs text-soft">
|
||||
{group.is_recruiting ? <span className="rounded-full bg-emerald-400/15 px-2.5 py-1 font-semibold text-emerald-200">Recruiting</span> : null}
|
||||
{group.is_verified ? <span className="rounded-full bg-sky-400/15 px-2.5 py-1 font-semibold text-sky-200">Verified</span> : null}
|
||||
{group.owner?.username || group.owner?.name ? <span>Led by {group.owner?.username || group.owner?.name}</span> : null}
|
||||
</div>
|
||||
|
||||
{stats.length > 0 ? (
|
||||
<div className="relative mt-4 flex flex-wrap gap-3 text-xs text-soft">
|
||||
{stats.map((item) => (
|
||||
<span key={item.key}>
|
||||
{item.value.toLocaleString()} {item.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<a
|
||||
href={group.urls?.public || '/groups'}
|
||||
className="relative mt-4 inline-flex w-fit rounded-lg bg-nova-700 px-4 py-1.5 text-xs font-semibold text-white transition hover:bg-nova-600"
|
||||
>
|
||||
View Group
|
||||
</a>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
export default function HomeGroups({ groups }) {
|
||||
const spotlightGroups = [
|
||||
groups?.spotlight,
|
||||
...(Array.isArray(groups?.featured) ? groups.featured : []),
|
||||
...(Array.isArray(groups?.recruiting) ? groups.recruiting : []),
|
||||
...(Array.isArray(groups?.rising) ? groups.rising : []),
|
||||
].filter(Boolean)
|
||||
|
||||
const uniqueGroups = spotlightGroups.filter((group, index, items) => (
|
||||
items.findIndex((candidate) => candidate?.id === group?.id) === index
|
||||
)).slice(0, 4)
|
||||
|
||||
if (uniqueGroups.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="mt-14 px-4 sm:px-6 lg:px-8">
|
||||
<div className="mb-5 flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold text-white">Group Spotlight</h2>
|
||||
<a href="/groups" className="text-sm text-nova-300 transition hover:text-white">
|
||||
All groups ->
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{uniqueGroups.map((group) => (
|
||||
<GroupSpotlightCard key={group.id} group={group} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -17,23 +17,25 @@ export default function HomeNews({ items }) {
|
||||
return (
|
||||
<section className="mt-14 px-4 sm:px-6 lg:px-8">
|
||||
<div className="mb-5 flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold text-white">📰 News & Updates</h2>
|
||||
<a href="/forum/news" className="text-sm text-nova-300 hover:text-white transition">
|
||||
<h2 className="text-xl font-bold text-white">News & Updates</h2>
|
||||
<a href="/news" className="text-sm text-nova-300 hover:text-white transition">
|
||||
All news →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-nova-800 rounded-xl bg-panel overflow-hidden">
|
||||
<div className="divide-y divide-nova-800 overflow-hidden rounded-[24px] border border-white/10 bg-panel">
|
||||
{items.map((item) => (
|
||||
<a
|
||||
key={item.id}
|
||||
href={item.url}
|
||||
className="flex items-start justify-between gap-4 px-5 py-4 transition hover:bg-nova-800"
|
||||
className="grid gap-3 px-5 py-4 transition hover:bg-nova-800 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-start"
|
||||
>
|
||||
<span className="text-sm font-medium text-white line-clamp-2">{item.title}</span>
|
||||
{item.date && (
|
||||
<span className="flex-shrink-0 text-xs text-soft">{formatDate(item.date)}</span>
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
{item.eyebrow ? <div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-nova-300">{item.eyebrow}</div> : null}
|
||||
<div className="mt-1 text-sm font-medium text-white line-clamp-2">{item.title}</div>
|
||||
{item.excerpt ? <p className="mt-2 text-sm leading-6 text-soft line-clamp-2">{item.excerpt}</p> : null}
|
||||
</div>
|
||||
{item.date ? <span className="flex-shrink-0 text-xs text-soft">{formatDate(item.date)}</span> : null}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ const HomeTrending = lazy(() => import('./HomeTrending'))
|
||||
const HomeRising = lazy(() => import('./HomeRising'))
|
||||
const HomeFresh = lazy(() => import('./HomeFresh'))
|
||||
const HomeCollections = lazy(() => import('./HomeCollections'))
|
||||
const HomeGroups = lazy(() => import('./HomeGroups'))
|
||||
const HomeCategories = lazy(() => import('./HomeCategories'))
|
||||
const HomeTags = lazy(() => import('./HomeTags'))
|
||||
const HomeCreators = lazy(() => import('./HomeCreators'))
|
||||
@@ -24,7 +25,7 @@ function SectionFallback() {
|
||||
}
|
||||
|
||||
function GuestHomePage(props) {
|
||||
const { rising, trending, fresh, tags, creators, news, collections_featured, collections_trending, collections_editorial, collections_community } = props
|
||||
const { rising, trending, fresh, tags, creators, news, collections_featured, collections_trending, collections_editorial, collections_community, groups } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -49,6 +50,10 @@ function GuestHomePage(props) {
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
<Suspense fallback={<SectionFallback />}>
|
||||
<HomeGroups groups={groups} />
|
||||
</Suspense>
|
||||
|
||||
{/* 4. Explore Categories */}
|
||||
<Suspense fallback={<SectionFallback />}>
|
||||
<HomeCategories />
|
||||
@@ -90,6 +95,7 @@ function AuthHomePage(props) {
|
||||
collections_trending,
|
||||
collections_editorial,
|
||||
collections_community,
|
||||
groups,
|
||||
by_categories,
|
||||
suggested_creators,
|
||||
tags,
|
||||
@@ -146,6 +152,10 @@ function AuthHomePage(props) {
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
<Suspense fallback={<SectionFallback />}>
|
||||
<HomeGroups groups={groups} />
|
||||
</Suspense>
|
||||
|
||||
{/* 4. Explore Categories */}
|
||||
<Suspense fallback={<SectionFallback />}>
|
||||
<HomeCategories />
|
||||
|
||||
Reference in New Issue
Block a user