Commit workspace changes
This commit is contained in:
@@ -2,12 +2,13 @@ import React, { useEffect, useState } from 'react'
|
||||
import { Link, usePage } from '@inertiajs/react'
|
||||
import { studioModule, studioSurface, trackStudioEvent } from '../utils/studioEvents'
|
||||
|
||||
const navGroups = [
|
||||
const baseNavGroups = [
|
||||
{
|
||||
label: 'Creator Studio',
|
||||
items: [
|
||||
{ label: 'Overview', href: '/studio', icon: 'fa-solid fa-chart-line' },
|
||||
{ label: 'Search', href: '/studio/search', icon: 'fa-solid fa-magnifying-glass' },
|
||||
{ label: 'Groups', href: '/studio/groups', icon: 'fa-solid fa-people-group' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -67,13 +68,82 @@ const navGroups = [
|
||||
},
|
||||
]
|
||||
|
||||
const quickCreateItems = [
|
||||
const baseQuickCreateItems = [
|
||||
{ label: 'Artwork', href: '/upload', icon: 'fa-solid fa-cloud-arrow-up' },
|
||||
{ label: 'Card', href: '/studio/cards/create', icon: 'fa-solid fa-id-card' },
|
||||
{ label: 'Story', href: '/creator/stories/create', icon: 'fa-solid fa-feather-pointed' },
|
||||
{ label: 'Collection', href: '/settings/collections/create', icon: 'fa-solid fa-layer-group' },
|
||||
]
|
||||
|
||||
const STUDIO_CONTEXT_STORAGE_KEY = 'sb.studio.last-context'
|
||||
const RESTORABLE_STUDIO_PATHS = ['/studio', '/studio/artworks', '/studio/collections', '/studio/settings']
|
||||
|
||||
function supportsStudioContextRestore(pathname) {
|
||||
return RESTORABLE_STUDIO_PATHS.includes(pathname)
|
||||
}
|
||||
|
||||
function studioRouteKeyForPath(pathname) {
|
||||
if (pathname === '/studio/artworks' || pathname.endsWith('/artworks')) return 'studio_artworks_url'
|
||||
if (pathname === '/studio/collections' || pathname.endsWith('/collections')) return 'studio_collections_url'
|
||||
if (pathname === '/studio/settings' || pathname.endsWith('/settings')) return 'studio_settings_url'
|
||||
if (pathname.endsWith('/members')) return 'studio_members_url'
|
||||
if (pathname.endsWith('/invitations')) return 'studio_invitations_url'
|
||||
|
||||
return 'studio_url'
|
||||
}
|
||||
|
||||
function nestedRouteKeyFor(topLevelRouteKey) {
|
||||
return topLevelRouteKey.replace(/_url$/, '')
|
||||
}
|
||||
|
||||
function groupStudioUrlForPath(group, pathname) {
|
||||
if (!group) return '/studio'
|
||||
|
||||
const routeKey = studioRouteKeyForPath(pathname)
|
||||
const nestedRouteKey = nestedRouteKeyFor(routeKey)
|
||||
|
||||
return group[routeKey] || group.urls?.[nestedRouteKey] || group.studio_url || group.urls?.studio || '/studio'
|
||||
}
|
||||
|
||||
function personalStudioUrlForPath(pathname) {
|
||||
if (pathname === '/studio/artworks' || pathname.endsWith('/artworks')) return '/studio/artworks'
|
||||
if (pathname === '/studio/collections' || pathname.endsWith('/collections')) return '/studio/collections'
|
||||
if (pathname === '/studio/settings' || pathname.endsWith('/settings')) return '/studio/settings'
|
||||
|
||||
return '/studio'
|
||||
}
|
||||
|
||||
function persistStudioContext(slug) {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
try {
|
||||
window.sessionStorage.setItem(STUDIO_CONTEXT_STORAGE_KEY, slug || '')
|
||||
} catch {
|
||||
// Ignore storage failures so Studio navigation keeps working.
|
||||
}
|
||||
}
|
||||
|
||||
function readPersistedStudioContext() {
|
||||
if (typeof window === 'undefined') return null
|
||||
|
||||
try {
|
||||
return window.sessionStorage.getItem(STUDIO_CONTEXT_STORAGE_KEY)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToStudioUrl(targetUrl) {
|
||||
if (typeof window === 'undefined' || !targetUrl) return
|
||||
|
||||
if (typeof window.location?.assign === 'function') {
|
||||
window.location.assign(targetUrl)
|
||||
return
|
||||
}
|
||||
|
||||
window.location.href = targetUrl
|
||||
}
|
||||
|
||||
function NavLink({ item, active }) {
|
||||
return (
|
||||
<Link
|
||||
@@ -91,10 +161,42 @@ function NavLink({ item, active }) {
|
||||
}
|
||||
|
||||
export default function StudioLayout({ children, title, subtitle, actions }) {
|
||||
const { url } = usePage()
|
||||
const { url, props } = usePage()
|
||||
const [mobileOpen, setMobileOpen] = useState(false)
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const pathname = url.split('?')[0]
|
||||
const studioGroups = Array.isArray(props.studio_groups) ? props.studio_groups : []
|
||||
const currentGroup = props.studioGroup || null
|
||||
const canManageNews = Boolean(props.auth?.user?.is_admin || props.auth?.user?.is_moderator)
|
||||
|
||||
const navGroups = baseNavGroups.map((group) => {
|
||||
if (!canManageNews || group.label !== 'Content') {
|
||||
return group
|
||||
}
|
||||
|
||||
return {
|
||||
...group,
|
||||
items: [
|
||||
...group.items,
|
||||
{ label: 'News', href: '/studio/news', icon: 'fa-solid fa-newspaper' },
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
const quickCreateItems = (canManageNews
|
||||
? [...baseQuickCreateItems, { label: 'News Article', href: '/studio/news/create', icon: 'fa-solid fa-newspaper' }]
|
||||
: baseQuickCreateItems
|
||||
).map((item) => {
|
||||
if (currentGroup?.urls && item.label === 'Artwork') {
|
||||
return { ...item, href: currentGroup.urls?.studio_artworks ? `/upload?group=${currentGroup.slug}` : item.href }
|
||||
}
|
||||
|
||||
if (currentGroup?.urls && item.label === 'Collection') {
|
||||
return { ...item, href: `/settings/collections/create?group=${currentGroup.slug}` }
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const moduleKey = studioModule(pathname)
|
||||
@@ -111,6 +213,31 @@ export default function StudioLayout({ children, title, subtitle, actions }) {
|
||||
})
|
||||
}, [pathname])
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentGroup?.slug) return
|
||||
|
||||
persistStudioContext(currentGroup.slug)
|
||||
}, [currentGroup?.slug])
|
||||
|
||||
useEffect(() => {
|
||||
if (currentGroup || !supportsStudioContextRestore(pathname)) return
|
||||
|
||||
const storedSlug = readPersistedStudioContext()
|
||||
if (!storedSlug) return
|
||||
|
||||
const nextGroup = studioGroups.find((group) => group.slug === storedSlug)
|
||||
if (!nextGroup) {
|
||||
persistStudioContext('')
|
||||
return
|
||||
}
|
||||
|
||||
const targetUrl = groupStudioUrlForPath(nextGroup, pathname)
|
||||
|
||||
if (targetUrl && targetUrl !== pathname) {
|
||||
navigateToStudioUrl(targetUrl)
|
||||
}
|
||||
}, [currentGroup, pathname, studioGroups])
|
||||
|
||||
const isActive = (href) => {
|
||||
if (href === '/studio') return pathname === '/studio'
|
||||
return pathname.startsWith(href)
|
||||
@@ -127,6 +254,19 @@ export default function StudioLayout({ children, title, subtitle, actions }) {
|
||||
})
|
||||
}
|
||||
|
||||
const handleContextChange = (nextSlug) => {
|
||||
persistStudioContext(nextSlug)
|
||||
|
||||
const nextGroup = studioGroups.find((group) => group.slug === nextSlug)
|
||||
const targetUrl = nextGroup
|
||||
? groupStudioUrlForPath(nextGroup, pathname)
|
||||
: personalStudioUrlForPath(pathname)
|
||||
|
||||
if (targetUrl !== pathname) {
|
||||
navigateToStudioUrl(targetUrl)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(14,165,233,0.12),_transparent_30%),radial-gradient(circle_at_bottom_right,_rgba(34,197,94,0.12),_transparent_35%),linear-gradient(180deg,_#06101d_0%,_#020617_45%,_#02040a_100%)]">
|
||||
<div className="sticky top-16 z-30 border-b border-white/10 bg-slate-950/80 backdrop-blur-xl lg:hidden">
|
||||
@@ -151,14 +291,31 @@ export default function StudioLayout({ children, title, subtitle, actions }) {
|
||||
className="absolute left-0 top-0 bottom-0 w-80 overflow-y-auto border-r border-white/10 bg-slate-950 p-4 pt-20"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<StudioSidebarContent isActive={isActive} onNavigate={() => setMobileOpen(false)} onQuickCreate={handleQuickCreateClick} />
|
||||
<StudioSidebarContent
|
||||
currentGroup={currentGroup}
|
||||
studioGroups={studioGroups}
|
||||
navGroups={navGroups}
|
||||
quickCreateItems={quickCreateItems}
|
||||
isActive={isActive}
|
||||
onNavigate={() => setMobileOpen(false)}
|
||||
onQuickCreate={handleQuickCreateClick}
|
||||
onContextChange={handleContextChange}
|
||||
/>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex">
|
||||
<aside className="sticky top-16 hidden min-h-[calc(100vh-4rem)] w-72 self-start border-r border-white/10 bg-slate-950/55 p-4 pt-6 backdrop-blur-xl lg:flex lg:flex-col">
|
||||
<StudioSidebarContent isActive={isActive} onQuickCreate={handleQuickCreateClick} />
|
||||
<StudioSidebarContent
|
||||
currentGroup={currentGroup}
|
||||
studioGroups={studioGroups}
|
||||
navGroups={navGroups}
|
||||
quickCreateItems={quickCreateItems}
|
||||
isActive={isActive}
|
||||
onQuickCreate={handleQuickCreateClick}
|
||||
onContextChange={handleContextChange}
|
||||
/>
|
||||
</aside>
|
||||
|
||||
<main className="min-w-0 flex-1 px-4 pb-10 pt-4 lg:px-8 lg:pt-6">
|
||||
@@ -168,9 +325,11 @@ export default function StudioLayout({ children, title, subtitle, actions }) {
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/70">Creator Studio</p>
|
||||
{title && <h1 className="mt-2 text-3xl font-semibold text-white lg:text-4xl">{title}</h1>}
|
||||
{subtitle && <p className="mt-3 max-w-2xl text-sm leading-6 text-slate-300">{subtitle}</p>}
|
||||
{currentGroup ? <p className="mt-4 inline-flex items-center gap-2 rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1 text-xs font-semibold uppercase tracking-[0.18em] text-sky-100">Group context: {currentGroup.name}</p> : null}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3 lg:justify-end">
|
||||
{studioGroups.length > 0 ? <ContextSwitcher currentGroup={currentGroup} studioGroups={studioGroups} onContextChange={handleContextChange} /> : null}
|
||||
{actions}
|
||||
<div className="relative">
|
||||
<button
|
||||
@@ -208,13 +367,48 @@ export default function StudioLayout({ children, title, subtitle, actions }) {
|
||||
)
|
||||
}
|
||||
|
||||
function StudioSidebarContent({ isActive, onNavigate, onQuickCreate }) {
|
||||
function ContextSwitcher({ currentGroup, studioGroups, onContextChange }) {
|
||||
return (
|
||||
<label className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-black/20 px-3 py-2 text-sm text-slate-200">
|
||||
<i className="fa-solid fa-people-group text-sky-200" />
|
||||
<select
|
||||
value={currentGroup?.slug || ''}
|
||||
onChange={(event) => onContextChange?.(event.target.value)}
|
||||
className="bg-transparent text-sm text-white outline-none"
|
||||
>
|
||||
<option value="" className="bg-slate-950 text-white">Personal studio</option>
|
||||
{studioGroups.map((group) => (
|
||||
<option key={group.slug} value={group.slug} className="bg-slate-950 text-white">
|
||||
{group.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
function StudioSidebarContent({ currentGroup, studioGroups, navGroups, quickCreateItems, isActive, onNavigate, onQuickCreate, onContextChange }) {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 rounded-[26px] border border-white/10 bg-white/[0.04] p-4">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/70">Skinbase Nova</p>
|
||||
<h2 className="mt-2 text-xl font-semibold text-white">Creator Studio</h2>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-400">Create, manage, and grow from one modular workspace built for every creator surface.</p>
|
||||
{studioGroups.length > 0 ? (
|
||||
<div className="mt-4 rounded-2xl border border-white/10 bg-black/20 p-3">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Context</p>
|
||||
<select
|
||||
value={currentGroup?.slug || ''}
|
||||
onChange={(event) => onContextChange?.(event.target.value)}
|
||||
className="mt-2 w-full rounded-xl border border-white/10 bg-slate-950/80 px-3 py-2 text-sm text-white outline-none"
|
||||
>
|
||||
<option value="">Personal studio</option>
|
||||
{studioGroups.map((group) => (
|
||||
<option key={group.slug} value={group.slug}>{group.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 space-y-5" onClick={onNavigate}>
|
||||
|
||||
Reference in New Issue
Block a user