Save workspace changes

This commit is contained in:
2026-04-18 17:02:56 +02:00
parent f02ea9a711
commit 87d60af5a9
4220 changed files with 1388603 additions and 1554 deletions

View File

@@ -0,0 +1,77 @@
import React from 'react'
import { usePage } from '@inertiajs/react'
import ProfileHero from '../../components/profile/ProfileHero'
import ProfileGalleryPanel from '../../components/profile/ProfileGalleryPanel'
export default function ProfileGallery() {
const { props } = usePage()
const {
user,
profile,
artworks,
featuredArtworks,
followerCount,
viewerIsFollowing,
heroBgUrl,
leaderboardRank,
countryName,
isOwner,
profileUrl,
} = props
const username = user.username || user.name
const displayName = user.name || user.username || 'Creator'
return (
<div className="min-h-screen pb-16">
<ProfileHero
user={user}
profile={profile}
isOwner={isOwner}
viewerIsFollowing={viewerIsFollowing}
followerCount={followerCount}
heroBgUrl={heroBgUrl}
countryName={countryName}
leaderboardRank={leaderboardRank}
extraActions={profileUrl ? (
<a
href={profileUrl}
className="inline-flex items-center gap-2 rounded-xl border border-white/15 px-4 py-2.5 text-sm font-medium text-slate-300 transition-all hover:bg-white/5 hover:text-white"
>
<i className="fa-solid fa-user fa-fw" />
View Profile
</a>
) : null}
/>
<div className="border-y border-white/10 bg-white/[0.02]">
<div className="mx-auto flex max-w-6xl flex-col gap-4 px-4 py-5 md:flex-row md:items-end md:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-sky-300/80">Public Gallery</p>
<h2 className="mt-1 text-2xl font-semibold tracking-tight text-white md:text-3xl">
{displayName}'s artworks
</h2>
<p className="mt-2 max-w-2xl text-sm leading-relaxed text-slate-400">
Browse published work with the same infinite-scroll gallery used across the profile experience.
</p>
</div>
<a
href={profileUrl || '#'}
className="inline-flex items-center gap-2 self-start rounded-xl border border-white/10 bg-white/[0.03] px-4 py-2.5 text-sm font-medium text-slate-300 transition-all hover:bg-white/[0.06] hover:text-white"
>
<i className="fa-solid fa-arrow-left fa-fw" />
Back to profile
</a>
</div>
</div>
<div className="w-full pt-6">
<ProfileGalleryPanel
artworks={artworks}
username={username}
/>
</div>
</div>
)
}

View File

@@ -0,0 +1,266 @@
import React, { useState, useEffect, useCallback } from 'react'
import { usePage } from '@inertiajs/react'
import ProfileHero from '../../components/profile/ProfileHero'
import ProfileTabs from '../../components/profile/ProfileTabs'
import TabArtworks from '../../components/profile/tabs/TabArtworks'
import TabAchievements from '../../components/profile/tabs/TabAchievements'
import TabAbout from '../../components/profile/tabs/TabAbout'
import TabStats from '../../components/profile/tabs/TabStats'
import TabFavourites from '../../components/profile/tabs/TabFavourites'
import TabCollections from '../../components/profile/tabs/TabCollections'
import TabActivity from '../../components/profile/tabs/TabActivity'
import TabPosts from '../../components/profile/tabs/TabPosts'
import TabStories from '../../components/profile/tabs/TabStories'
import GroupProfileSummary from '../../components/groups/GroupProfileSummary'
const VALID_TABS = ['posts', 'artworks', 'stories', 'achievements', 'collections', 'about', 'stats', 'favourites', 'activity']
function getInitialTab(initialTab = 'posts') {
if (typeof window === 'undefined') {
return VALID_TABS.includes(initialTab) ? initialTab : 'posts'
}
try {
const pathname = window.location.pathname.replace(/\/+$/, '')
const segments = pathname.split('/').filter(Boolean)
const lastSegment = segments.at(-1)
if (VALID_TABS.includes(lastSegment)) {
return lastSegment
}
} catch {
return VALID_TABS.includes(initialTab) ? initialTab : 'posts'
}
return VALID_TABS.includes(initialTab) ? initialTab : 'posts'
}
/**
* ProfileShow Inertia page for /@username
*
* Props injected by ProfileController::renderUserProfile()
*/
export default function ProfileShow() {
const { props } = usePage()
const {
user,
profile,
artworks,
featuredArtworks,
favourites,
stats,
socialLinks,
followerCount,
recentFollowers,
followContext,
followAnalytics,
suggestedUsers,
viewerIsFollowing,
heroBgUrl,
profileComments,
creatorStories,
collections,
achievements,
leaderboardRank,
groupContributionHistory,
countryName,
isOwner,
auth,
initialTab,
profileUrl,
galleryUrl,
collectionCreateUrl,
collectionReorderUrl,
collectionsFeaturedUrl,
collectionFeatureLimit,
profileTabUrls,
} = props
const [activeTab, setActiveTab] = useState(() => getInitialTab(initialTab))
const handleTabChange = useCallback((tab) => {
if (!VALID_TABS.includes(tab)) return
setActiveTab(tab)
try {
const currentUrl = new URL(window.location.href)
const targetBase = profileTabUrls?.[tab] || `${profileUrl || `${window.location.origin}`}/${tab}`
const nextUrl = new URL(targetBase, window.location.origin)
const sharedPostId = currentUrl.searchParams.get('post')
if (sharedPostId) {
nextUrl.searchParams.set('post', sharedPostId)
}
window.history.pushState({}, '', nextUrl.toString())
} catch (_) {}
}, [profileTabUrls, profileUrl])
useEffect(() => {
const onPop = () => setActiveTab(getInitialTab(initialTab))
window.addEventListener('popstate', onPop)
return () => window.removeEventListener('popstate', onPop)
}, [initialTab])
const isLoggedIn = !!(auth?.user)
// Normalise artwork list (SSR may send cursor-paginated object)
const artworkList = Array.isArray(artworks)
? artworks
: (artworks?.data ?? [])
const artworkNextCursor = artworks?.next_cursor ?? null
const favouriteList = Array.isArray(favourites)
? favourites
: (favourites?.data ?? [])
const favouriteNextCursor = favourites?.next_cursor ?? null
// Normalise social links (may be object keyed by platform, or array)
const socialLinksObj = Array.isArray(socialLinks)
? socialLinks.reduce((acc, l) => { acc[l.platform] = l; return acc }, {})
: (socialLinks ?? {})
const contentShellClassName = activeTab === 'artworks'
? 'w-full px-4 md:px-6'
: activeTab === 'posts'
? 'mx-auto max-w-7xl px-4 md:px-6'
: 'max-w-6xl mx-auto px-4'
return (
<div className="relative min-h-screen overflow-hidden pb-16">
<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(249,115,22,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' }}
/>
<ProfileHero
user={user}
profile={profile}
isOwner={isOwner}
viewerIsFollowing={viewerIsFollowing}
followerCount={followerCount}
recentFollowers={recentFollowers}
followContext={followContext}
heroBgUrl={heroBgUrl}
countryName={countryName}
leaderboardRank={leaderboardRank}
extraActions={galleryUrl ? (
<a
href={galleryUrl}
className="inline-flex shrink-0 items-center gap-2 whitespace-nowrap rounded-xl border border-white/15 px-3.5 py-2 text-sm font-medium text-slate-300 transition-all hover:bg-white/5 hover:text-white"
>
<i className="fa-solid fa-images fa-fw" />
View Gallery
</a>
) : null}
/>
<div className="mt-6">
<ProfileTabs
activeTab={activeTab}
onTabChange={handleTabChange}
/>
</div>
<GroupProfileSummary contributions={groupContributionHistory} href={profileTabUrls?.about} />
<div className={`${contentShellClassName} pt-6`}>
{activeTab === 'artworks' && (
<TabArtworks
artworks={{ data: artworkList, next_cursor: artworkNextCursor }}
featuredArtworks={featuredArtworks}
username={user.username || user.name}
galleryUrl={galleryUrl}
isActive
/>
)}
{activeTab === 'posts' && (
<TabPosts
username={user.username || user.name}
isOwner={isOwner}
authUser={auth?.user ?? null}
user={user}
profile={profile}
stats={stats}
followerCount={followerCount}
recentFollowers={recentFollowers}
suggestedUsers={suggestedUsers}
socialLinks={socialLinksObj}
countryName={countryName}
profileUrl={profileUrl}
onTabChange={handleTabChange}
/>
)}
{activeTab === 'stories' && (
<TabStories
stories={creatorStories}
username={user.username || user.name}
/>
)}
{activeTab === 'achievements' && (
<TabAchievements achievements={achievements} />
)}
{activeTab === 'collections' && (
<TabCollections
collections={collections}
isOwner={isOwner}
createUrl={collectionCreateUrl}
reorderUrl={collectionReorderUrl}
featuredUrl={collectionsFeaturedUrl}
featureLimit={collectionFeatureLimit}
/>
)}
{activeTab === 'about' && (
<TabAbout
user={user}
profile={profile}
stats={stats}
achievements={achievements}
artworks={artworkList}
creatorStories={creatorStories}
profileComments={profileComments}
socialLinks={socialLinksObj}
countryName={countryName}
followerCount={followerCount}
recentFollowers={recentFollowers}
leaderboardRank={leaderboardRank}
groupContributionHistory={groupContributionHistory}
/>
)}
{activeTab === 'stats' && (
<TabStats
stats={stats}
followerCount={followerCount}
followAnalytics={followAnalytics}
/>
)}
{activeTab === 'favourites' && (
<TabFavourites
favourites={{ data: favouriteList, next_cursor: favouriteNextCursor }}
isOwner={isOwner}
username={user.username || user.name}
/>
)}
{activeTab === 'activity' && (
<TabActivity
profileComments={profileComments}
user={user}
isOwner={isOwner}
isLoggedIn={isLoggedIn}
stats={stats}
followerCount={followerCount}
creatorStories={creatorStories}
/>
)}
</div>
</div>
)
}