Implement creator studio and upload updates
This commit is contained in:
@@ -1,13 +1,77 @@
|
||||
import React, { useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Link, usePage } from '@inertiajs/react'
|
||||
import { studioModule, studioSurface, trackStudioEvent } from '../utils/studioEvents'
|
||||
|
||||
const navItems = [
|
||||
{ label: 'Overview', href: '/studio', icon: 'fa-solid fa-chart-line' },
|
||||
{ label: 'Artworks', href: '/studio/artworks', icon: 'fa-solid fa-images' },
|
||||
{ label: 'Cards', href: '/studio/cards', icon: 'fa-solid fa-rectangle-history-circle-user' },
|
||||
{ label: 'Drafts', href: '/studio/artworks/drafts', icon: 'fa-solid fa-file-pen' },
|
||||
{ label: 'Archived', href: '/studio/artworks/archived', icon: 'fa-solid fa-box-archive' },
|
||||
{ label: 'Analytics', href: '/studio/analytics', icon: 'fa-solid fa-chart-pie' },
|
||||
const navGroups = [
|
||||
{
|
||||
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: 'Create',
|
||||
items: [
|
||||
{ label: 'New Artwork', href: '/upload', icon: 'fa-solid fa-cloud-arrow-up' },
|
||||
{ label: 'New Card', href: '/studio/cards/create', icon: 'fa-solid fa-id-card' },
|
||||
{ label: 'New Story', href: '/creator/stories/create', icon: 'fa-solid fa-feather-pointed' },
|
||||
{ label: 'New Collection', href: '/settings/collections/create', icon: 'fa-solid fa-layer-group' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Content',
|
||||
items: [
|
||||
{ label: 'All Content', href: '/studio/content', icon: 'fa-solid fa-table-cells-large' },
|
||||
{ label: 'Artworks', href: '/studio/artworks', icon: 'fa-solid fa-images' },
|
||||
{ label: 'Cards', href: '/studio/cards', icon: 'fa-solid fa-id-card' },
|
||||
{ label: 'Collections', href: '/studio/collections', icon: 'fa-solid fa-layer-group' },
|
||||
{ label: 'Stories', href: '/studio/stories', icon: 'fa-solid fa-feather-pointed' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Library',
|
||||
items: [
|
||||
{ label: 'Drafts', href: '/studio/drafts', icon: 'fa-solid fa-file-pen' },
|
||||
{ label: 'Scheduled', href: '/studio/scheduled', icon: 'fa-solid fa-calendar-days' },
|
||||
{ label: 'Calendar', href: '/studio/calendar', icon: 'fa-solid fa-calendar-range' },
|
||||
{ label: 'Archived', href: '/studio/archived', icon: 'fa-solid fa-box-archive' },
|
||||
{ label: 'Assets', href: '/studio/assets', icon: 'fa-solid fa-photo-film' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Engagement',
|
||||
items: [
|
||||
{ label: 'Inbox', href: '/studio/inbox', icon: 'fa-solid fa-inbox' },
|
||||
{ label: 'Activity', href: '/studio/activity', icon: 'fa-solid fa-bell' },
|
||||
{ label: 'Comments', href: '/studio/comments', icon: 'fa-solid fa-comments' },
|
||||
{ label: 'Followers', href: '/studio/followers', icon: 'fa-solid fa-user-group' },
|
||||
{ label: 'Challenges', href: '/studio/challenges', icon: 'fa-solid fa-trophy' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Insights',
|
||||
items: [
|
||||
{ label: 'Analytics', href: '/studio/analytics', icon: 'fa-solid fa-chart-pie' },
|
||||
{ label: 'Growth', href: '/studio/growth', icon: 'fa-solid fa-chart-line' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Creator',
|
||||
items: [
|
||||
{ label: 'Profile', href: '/studio/profile', icon: 'fa-solid fa-id-card' },
|
||||
{ label: 'Featured Content', href: '/studio/featured', icon: 'fa-solid fa-wand-magic-sparkles' },
|
||||
{ label: 'Preferences', href: '/studio/preferences', icon: 'fa-solid fa-sliders' },
|
||||
{ label: 'Studio Settings', href: '/studio/settings', icon: 'fa-solid fa-gear' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const quickCreateItems = [
|
||||
{ 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' },
|
||||
]
|
||||
|
||||
function NavLink({ item, active }) {
|
||||
@@ -26,53 +90,117 @@ function NavLink({ item, active }) {
|
||||
)
|
||||
}
|
||||
|
||||
export default function StudioLayout({ children, title }) {
|
||||
const { url, props } = usePage()
|
||||
const user = props.auth?.user
|
||||
export default function StudioLayout({ children, title, subtitle, actions }) {
|
||||
const { url } = usePage()
|
||||
const [mobileOpen, setMobileOpen] = useState(false)
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const pathname = url.split('?')[0]
|
||||
|
||||
useEffect(() => {
|
||||
const moduleKey = studioModule(pathname)
|
||||
const surface = studioSurface(pathname)
|
||||
|
||||
trackStudioEvent('studio_opened', {
|
||||
surface,
|
||||
module: moduleKey,
|
||||
})
|
||||
|
||||
trackStudioEvent('studio_module_opened', {
|
||||
surface,
|
||||
module: moduleKey,
|
||||
})
|
||||
}, [pathname])
|
||||
|
||||
const isActive = (href) => {
|
||||
if (href === '/studio') return url === '/studio'
|
||||
return url.startsWith(href)
|
||||
if (href === '/studio') return pathname === '/studio'
|
||||
return pathname.startsWith(href)
|
||||
}
|
||||
|
||||
const handleQuickCreateClick = (item) => {
|
||||
trackStudioEvent('studio_quick_create_used', {
|
||||
surface: studioSurface(pathname),
|
||||
module: item.label.toLowerCase(),
|
||||
meta: {
|
||||
href: item.href,
|
||||
label: item.label,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-nova-900">
|
||||
{/* Mobile top bar */}
|
||||
<div className="lg:hidden flex items-center justify-between px-4 py-3 border-b border-white/10 bg-nova-900/80 backdrop-blur-xl sticky top-16 z-30">
|
||||
<h1 className="text-lg font-bold text-white">Studio</h1>
|
||||
<button
|
||||
onClick={() => setMobileOpen(!mobileOpen)}
|
||||
className="text-slate-400 hover:text-white p-2"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<i className={`fa-solid ${mobileOpen ? 'fa-xmark' : 'fa-bars'} text-xl`} />
|
||||
</button>
|
||||
<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">
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/70">Creator Studio</p>
|
||||
<h1 className="text-lg font-bold text-white">{title || 'Creator Studio'}</h1>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setMobileOpen(!mobileOpen)}
|
||||
className="rounded-full border border-white/10 p-2 text-slate-400 hover:text-white"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<i className={`fa-solid ${mobileOpen ? 'fa-xmark' : 'fa-bars'} text-xl`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile nav overlay */}
|
||||
{mobileOpen && (
|
||||
<div className="lg:hidden fixed inset-0 z-40 bg-black/60 backdrop-blur-sm" onClick={() => setMobileOpen(false)}>
|
||||
<div className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm lg:hidden" onClick={() => setMobileOpen(false)}>
|
||||
<nav
|
||||
className="absolute left-0 top-0 bottom-0 w-72 bg-nova-900 border-r border-white/10 p-4 pt-20 space-y-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
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)} />
|
||||
<StudioSidebarContent isActive={isActive} onNavigate={() => setMobileOpen(false)} onQuickCreate={handleQuickCreateClick} />
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex">
|
||||
{/* Desktop sidebar */}
|
||||
<aside className="hidden lg:flex flex-col w-64 min-h-[calc(100vh-4rem)] border-r border-white/10 bg-nova-900/60 backdrop-blur-xl p-4 pt-6 sticky top-16 self-start">
|
||||
<StudioSidebarContent isActive={isActive} />
|
||||
<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} />
|
||||
</aside>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 min-w-0 px-4 lg:px-8 pt-4 pb-8">
|
||||
{title && (
|
||||
<h1 className="text-2xl font-bold text-white mb-4">{title}</h1>
|
||||
)}
|
||||
<main className="min-w-0 flex-1 px-4 pb-10 pt-4 lg:px-8 lg:pt-6">
|
||||
<section className="mb-6 rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,_rgba(34,197,94,0.12),_transparent_28%),radial-gradient(circle_at_bottom_right,_rgba(56,189,248,0.12),_transparent_35%),linear-gradient(135deg,_rgba(15,23,42,0.84),_rgba(2,6,23,0.95))] p-5 shadow-[0_22px_70px_rgba(2,6,23,0.32)] lg:p-6">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div className="max-w-3xl">
|
||||
<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>}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3 lg:justify-end">
|
||||
{actions}
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCreateOpen((current) => !current)}
|
||||
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/35 hover:bg-sky-300/15"
|
||||
>
|
||||
<i className="fa-solid fa-plus" />
|
||||
Create New
|
||||
</button>
|
||||
{createOpen && (
|
||||
<div className="absolute right-0 top-[calc(100%+0.75rem)] z-20 min-w-[220px] rounded-[24px] border border-white/10 bg-slate-950/95 p-2 shadow-[0_18px_40px_rgba(2,6,23,0.5)] backdrop-blur-xl">
|
||||
{quickCreateItems.map((item) => (
|
||||
<a
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={() => handleQuickCreateClick(item)}
|
||||
className="flex items-center gap-3 rounded-2xl px-4 py-3 text-sm text-slate-200 transition hover:bg-white/[0.06] hover:text-white"
|
||||
>
|
||||
<i className={`${item.icon} w-5 text-center text-sky-200`} />
|
||||
<span>New {item.label}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
@@ -80,28 +208,46 @@ export default function StudioLayout({ children, title }) {
|
||||
)
|
||||
}
|
||||
|
||||
function StudioSidebarContent({ isActive, onNavigate }) {
|
||||
function StudioSidebarContent({ isActive, onNavigate, onQuickCreate }) {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xs font-semibold uppercase tracking-wider text-slate-500 px-4 mb-2">Creator Studio</h2>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<nav className="space-y-1 flex-1" onClick={onNavigate}>
|
||||
{navItems.map((item) => (
|
||||
<NavLink key={item.href} item={item} active={isActive(item.href)} />
|
||||
<nav className="flex-1 space-y-5" onClick={onNavigate}>
|
||||
{navGroups.map((group) => (
|
||||
<div key={group.label}>
|
||||
<h3 className="mb-2 px-3 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">{group.label}</h3>
|
||||
<div className="space-y-1">
|
||||
{group.items.map((item) => (
|
||||
<NavLink key={item.href} item={item} active={isActive(item.href)} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="mt-auto pt-6">
|
||||
<Link
|
||||
href="/upload"
|
||||
className="flex items-center justify-center gap-2 w-full px-4 py-3 rounded-xl bg-sky-600 hover:bg-sky-500 text-white font-semibold text-sm transition-all duration-200 shadow-lg"
|
||||
onClick={onNavigate}
|
||||
>
|
||||
<i className="fa-solid fa-cloud-arrow-up" />
|
||||
Upload
|
||||
</Link>
|
||||
<div className="mt-6 rounded-[24px] border border-white/10 bg-[linear-gradient(135deg,_rgba(15,23,42,0.95),_rgba(12,74,110,0.4))] p-4">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-sky-100/70">Quick create</p>
|
||||
<div className="mt-3 grid gap-2">
|
||||
{quickCreateItems.map((item) => (
|
||||
<a
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="inline-flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-100 transition hover:border-white/20 hover:bg-white/[0.06]"
|
||||
onClick={() => {
|
||||
onQuickCreate?.(item)
|
||||
onNavigate?.()
|
||||
}}
|
||||
>
|
||||
<i className={`${item.icon} w-5 text-center text-sky-200`} />
|
||||
<span>New {item.label}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user