Implement creator studio and upload updates
This commit is contained in:
@@ -1,141 +1,536 @@
|
||||
import React from 'react'
|
||||
import { usePage, Link } from '@inertiajs/react'
|
||||
import { usePage } from '@inertiajs/react'
|
||||
import StudioLayout from '../../Layouts/StudioLayout'
|
||||
import { studioSurface, trackStudioEvent } from '../../utils/studioEvents'
|
||||
|
||||
const kpiConfig = [
|
||||
{ key: 'total_artworks', label: 'Total Artworks', icon: 'fa-images', color: 'text-blue-400', link: '/studio/artworks' },
|
||||
{ key: 'views_30d', label: 'Views (30d)', icon: 'fa-eye', color: 'text-emerald-400', link: null },
|
||||
{ key: 'favourites_30d', label: 'Favourites (30d)', icon: 'fa-heart', color: 'text-pink-400', link: null },
|
||||
{ key: 'shares_30d', label: 'Shares (30d)', icon: 'fa-share-nodes', color: 'text-amber-400', link: null },
|
||||
{ key: 'followers', label: 'Followers', icon: 'fa-user-group', color: 'text-purple-400', link: null },
|
||||
{ key: 'total_content', label: 'Total content', icon: 'fa-solid fa-table-cells-large' },
|
||||
{ key: 'views_30d', label: 'Views', icon: 'fa-solid fa-eye' },
|
||||
{ key: 'appreciation_30d', label: 'Reactions', icon: 'fa-solid fa-heart' },
|
||||
{ key: 'shares_30d', label: 'Shares / Saves', icon: 'fa-solid fa-share-nodes' },
|
||||
{ key: 'comments_30d', label: 'Comments', icon: 'fa-solid fa-comments' },
|
||||
{ key: 'followers', label: 'Followers', icon: 'fa-solid fa-user-group' },
|
||||
]
|
||||
|
||||
function KpiCard({ config, value }) {
|
||||
const content = (
|
||||
<div className="bg-nova-900/60 border border-white/10 rounded-2xl p-5 hover:border-white/20 hover:shadow-lg hover:shadow-accent/5 transition-all duration-300 cursor-pointer group">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className={`w-10 h-10 rounded-xl bg-white/5 flex items-center justify-center ${config.color} group-hover:scale-110 transition-transform`}>
|
||||
<i className={`fa-solid ${config.icon}`} />
|
||||
return (
|
||||
<div className="rounded-[26px] border border-white/10 bg-white/[0.03] p-5 shadow-[0_18px_40px_rgba(2,6,23,0.18)]">
|
||||
<div className="flex items-center gap-3 text-slate-300">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-sky-300/10 text-sky-100">
|
||||
<i className={config.icon} />
|
||||
</div>
|
||||
<span className="text-xs font-medium text-slate-400 uppercase tracking-wider">{config.label}</span>
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">{config.label}</span>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-white tabular-nums">
|
||||
{typeof value === 'number' ? value.toLocaleString() : value}
|
||||
</p>
|
||||
<p className="mt-4 text-3xl font-semibold text-white tabular-nums">{typeof value === 'number' ? value.toLocaleString() : value}</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (config.link) {
|
||||
return <Link href={config.link}>{content}</Link>
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
function TopPerformerCard({ artwork }) {
|
||||
function QuickCreateCard({ item }) {
|
||||
return (
|
||||
<div className="bg-nova-900/60 border border-white/10 rounded-2xl p-4 hover:border-white/20 hover:shadow-lg hover:shadow-accent/5 transition-all duration-300 group">
|
||||
<div className="flex items-start gap-3">
|
||||
{artwork.thumb_url && (
|
||||
<img
|
||||
src={artwork.thumb_url}
|
||||
alt={artwork.title}
|
||||
className="w-16 h-16 rounded-xl object-cover bg-nova-800 flex-shrink-0 group-hover:scale-105 transition-transform"
|
||||
loading="lazy"
|
||||
/>
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<h4 className="text-sm font-semibold text-white truncate" title={artwork.title}>
|
||||
{artwork.title}
|
||||
</h4>
|
||||
<div className="flex flex-wrap items-center gap-3 mt-1.5">
|
||||
<span className="text-xs text-slate-400">
|
||||
❤️ {artwork.favourites?.toLocaleString()}
|
||||
</span>
|
||||
<span className="text-xs text-slate-400">
|
||||
🔗 {artwork.shares?.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
{artwork.heat_score > 5 && (
|
||||
<span className="inline-flex items-center gap-1 mt-2 px-2 py-0.5 rounded-md text-[10px] font-medium bg-orange-500/20 text-orange-400 border border-orange-500/30">
|
||||
<i className="fa-solid fa-fire" /> Rising
|
||||
</span>
|
||||
)}
|
||||
<a
|
||||
href={item.url}
|
||||
onClick={() => trackStudioEvent('studio_quick_create_used', {
|
||||
surface: studioSurface(),
|
||||
module: item.key,
|
||||
meta: {
|
||||
href: item.url,
|
||||
label: item.label,
|
||||
},
|
||||
})}
|
||||
className="rounded-[24px] border border-sky-300/20 bg-sky-300/10 p-4 text-sky-100 transition hover:border-sky-300/35 hover:bg-sky-300/15"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<i className={item.icon} />
|
||||
<span className="text-sm font-semibold">New {item.label}</span>
|
||||
</div>
|
||||
<p className="mt-3 text-sm leading-6 text-sky-100/80">Jump straight into the dedicated {item.label.toLowerCase()} creation workflow.</p>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
function RecentPublishCard({ item }) {
|
||||
return (
|
||||
<a href={item.edit_url || item.manage_url} className="block rounded-[24px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/70">{item.module_label}</p>
|
||||
<h3 className="mt-2 text-base font-semibold text-white">{item.title}</h3>
|
||||
<p className="mt-1 text-sm text-slate-400">Published {new Date(item.published_at || item.updated_at).toLocaleDateString()}</p>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
function ContinueWorkingCard({ item }) {
|
||||
return (
|
||||
<a href={item.edit_url || item.manage_url} className="block rounded-[24px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/70">{item.module_label}</p>
|
||||
<h3 className="mt-2 text-base font-semibold text-white">{item.title}</h3>
|
||||
<p className="mt-1 text-sm text-slate-400">Updated {new Date(item.updated_at || item.created_at).toLocaleDateString()}</p>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
function ScheduledItemCard({ item }) {
|
||||
return (
|
||||
<a href={item.edit_url || item.manage_url} className="block rounded-[24px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/70">{item.module_label}</p>
|
||||
<h3 className="mt-2 text-base font-semibold text-white">{item.title}</h3>
|
||||
<p className="mt-1 text-sm text-slate-400">Scheduled {new Date(item.scheduled_at || item.published_at).toLocaleString()}</p>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
function ActivityRow({ item }) {
|
||||
return (
|
||||
<a href={item.url} className="block rounded-[20px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-sm font-semibold text-white">{item.title}</p>
|
||||
<span className="text-[11px] uppercase tracking-[0.18em] text-slate-500">{item.module_label}</span>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-slate-400 line-clamp-2">{item.body}</p>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
function GrowthHint({ item }) {
|
||||
return (
|
||||
<a href={item.url} className="block rounded-[24px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20">
|
||||
<h3 className="text-base font-semibold text-white">{item.title}</h3>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-400">{item.body}</p>
|
||||
<span className="mt-4 inline-flex items-center gap-2 text-sm font-medium text-sky-100">
|
||||
{item.label}
|
||||
<i className="fa-solid fa-arrow-right" />
|
||||
</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
function ChallengeWidget({ challenge }) {
|
||||
return (
|
||||
<a href={challenge.url} className="block rounded-[24px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/70">{challenge.official ? 'Official challenge' : 'Community challenge'}</p>
|
||||
<h3 className="mt-2 text-base font-semibold text-white">{challenge.title}</h3>
|
||||
</div>
|
||||
<span className="text-[10px] uppercase tracking-[0.16em] text-slate-500">{challenge.status}</span>
|
||||
</div>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-400 line-clamp-3">{challenge.prompt || challenge.description}</p>
|
||||
<div className="mt-3 flex flex-wrap gap-3 text-xs text-slate-500">
|
||||
<span>{Number(challenge.entries_count || 0).toLocaleString()} entries</span>
|
||||
<span>{challenge.is_joined ? `${challenge.submission_count} submitted` : 'Not joined'}</span>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
function FeaturedStatusCard({ item }) {
|
||||
return (
|
||||
<a href={item.edit_url || item.manage_url || item.view_url} className="block rounded-[24px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/70">{item.module_label}</p>
|
||||
<h3 className="mt-2 text-base font-semibold text-white">{item.title}</h3>
|
||||
<p className="mt-1 text-sm text-slate-400">{item.subtitle || item.visibility || 'Selected for profile presentation'}</p>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandCenterColumn({ title, items = [], empty, renderItem }) {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold uppercase tracking-[0.18em] text-slate-500">{title}</h3>
|
||||
<div className="mt-3 space-y-3">
|
||||
{items.length > 0 ? items.map(renderItem) : <div className="rounded-2xl border border-dashed border-white/10 px-4 py-6 text-sm text-slate-500">{empty}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function InsightBlock({ item }) {
|
||||
const toneClasses = {
|
||||
positive: 'border-emerald-400/20 bg-emerald-400/10 text-emerald-100',
|
||||
warning: 'border-amber-400/20 bg-amber-400/10 text-amber-100',
|
||||
action: 'border-sky-400/20 bg-sky-400/10 text-sky-100',
|
||||
neutral: 'border-white/10 bg-white/[0.03] text-slate-200',
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={item.href}
|
||||
onClick={() => trackStudioEvent('studio_insight_clicked', {
|
||||
surface: studioSurface(),
|
||||
module: 'overview',
|
||||
meta: {
|
||||
insight_key: item.key,
|
||||
href: item.href,
|
||||
},
|
||||
})}
|
||||
className={`block rounded-[24px] border p-4 transition hover:border-white/20 ${toneClasses[item.tone] || toneClasses.neutral}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-2xl border border-white/10 bg-black/20">
|
||||
<i className={item.icon} />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-base font-semibold text-white">{item.title}</h3>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-300">{item.body}</p>
|
||||
<span className="mt-3 inline-flex items-center gap-2 text-sm font-medium text-inherit">
|
||||
{item.cta}
|
||||
<i className="fa-solid fa-arrow-right" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
function TopPerformerCard({ item }) {
|
||||
return (
|
||||
<article className="rounded-[26px] border border-white/10 bg-white/[0.03] p-4 transition hover:border-white/20">
|
||||
<div className="flex items-start gap-3">
|
||||
{item.image_url && (
|
||||
<img src={item.image_url} alt={item.title} className="h-16 w-16 flex-shrink-0 rounded-2xl object-cover" loading="lazy" />
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/70">{item.module_label}</p>
|
||||
<h4 className="mt-1 truncate text-base font-semibold text-white" title={item.title}>{item.title}</h4>
|
||||
<p className="mt-1 text-sm text-slate-400">{item.subtitle || item.visibility}</p>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-3 text-xs text-slate-400">
|
||||
<span>{Number(item.metrics?.views || 0).toLocaleString()} views</span>
|
||||
<span>{Number(item.metrics?.appreciation || 0).toLocaleString()} reactions</span>
|
||||
<span>{Number(item.metrics?.comments || 0).toLocaleString()} comments</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<a href={item.edit_url || item.manage_url} className="inline-flex items-center gap-2 rounded-full border border-white/10 px-3 py-1.5 text-xs text-slate-100">Edit</a>
|
||||
<a href={item.analytics_url} className="inline-flex items-center gap-2 rounded-full border border-white/10 px-3 py-1.5 text-xs text-slate-100">Analytics</a>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
function RecentComment({ comment }) {
|
||||
return (
|
||||
<div className="flex items-start gap-3 py-3 border-b border-white/5 last:border-0">
|
||||
<div className="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center text-xs text-slate-400 flex-shrink-0">
|
||||
<i className="fa-solid fa-comment" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm text-white">
|
||||
<span className="font-medium text-accent">{comment.author_name}</span>
|
||||
{' '}on{' '}
|
||||
<span className="text-slate-300">{comment.artwork_title}</span>
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-0.5 line-clamp-2">{comment.body}</p>
|
||||
<p className="text-[10px] text-slate-600 mt-1">
|
||||
{new Date(comment.created_at).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="border-b border-white/5 py-3 last:border-0">
|
||||
<p className="text-sm text-white">
|
||||
<span className="font-medium text-sky-100">{comment.author_name}</span>
|
||||
{' '}on{' '}
|
||||
<span className="text-slate-300">{comment.item_title}</span>
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-slate-500 line-clamp-2">{comment.body}</p>
|
||||
<p className="mt-1 text-[10px] text-slate-600">{new Date(comment.created_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function StudioDashboard() {
|
||||
const { props } = usePage()
|
||||
const { kpis, topPerformers, recentComments } = props
|
||||
const overview = props.overview || {}
|
||||
const analytics = props.analytics || {}
|
||||
const kpis = overview.kpis || {}
|
||||
const widgetVisibility = overview.preferences?.widget_visibility || {}
|
||||
|
||||
const showWidget = (key) => widgetVisibility[key] !== false
|
||||
|
||||
return (
|
||||
<StudioLayout title="Studio Overview">
|
||||
{/* KPI Cards */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4 mb-8">
|
||||
<StudioLayout title="Overview" subtitle="Create, manage, and grow across artworks, cards, collections, and stories from one shared creator operating surface.">
|
||||
<div className="grid grid-cols-2 gap-4 lg:grid-cols-5">
|
||||
{kpiConfig.map((config) => (
|
||||
<KpiCard key={config.key} config={config} value={kpis?.[config.key] ?? 0} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Top Performers */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-white">Your Top Performers</h2>
|
||||
<span className="text-xs text-slate-500">Last 7 days</span>
|
||||
</div>
|
||||
{topPerformers?.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{topPerformers.map((art) => (
|
||||
<TopPerformerCard key={art.id} artwork={art} />
|
||||
<div className="mt-6 grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<section className="rounded-[30px] border border-white/10 bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.14),_transparent_35%),linear-gradient(135deg,_rgba(15,23,42,0.86),_rgba(2,6,23,0.96))] p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-white">Command center</h2>
|
||||
<a href="/studio/calendar" className="text-sm font-medium text-sky-100">Open calendar</a>
|
||||
</div>
|
||||
<div className="mt-5 grid gap-5 lg:grid-cols-3">
|
||||
<CommandCenterColumn
|
||||
title="Publishing today"
|
||||
items={overview.command_center?.publishing_today || []}
|
||||
empty="Nothing is scheduled today."
|
||||
renderItem={(item) => <ScheduledItemCard key={item.id} item={item} />}
|
||||
/>
|
||||
<CommandCenterColumn
|
||||
title="Attention now"
|
||||
items={overview.command_center?.attention_now || []}
|
||||
empty="Inbox is quiet right now."
|
||||
renderItem={(item) => <ActivityRow key={item.id} item={item} />}
|
||||
/>
|
||||
<CommandCenterColumn
|
||||
title="Ready to schedule"
|
||||
items={overview.workflow_focus?.ready_to_schedule || []}
|
||||
empty="No ready drafts yet."
|
||||
renderItem={(item) => <ContinueWorkingCard key={item.id} item={item} />}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-lg font-semibold text-white">Readable insights</h2>
|
||||
<div className="mt-4 space-y-3">
|
||||
{(overview.insight_blocks || []).map((item) => (
|
||||
<InsightBlock key={item.key} item={item} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-nova-900/40 border border-white/10 rounded-2xl p-8 text-center">
|
||||
<p className="text-slate-500 text-sm">No artworks yet. Upload your first creation!</p>
|
||||
<Link
|
||||
href="/upload"
|
||||
className="inline-flex items-center gap-2 mt-4 px-5 py-2.5 rounded-xl bg-accent hover:bg-accent/90 text-white text-sm font-semibold transition-all shadow-lg shadow-accent/25"
|
||||
>
|
||||
<i className="fa-solid fa-cloud-arrow-up" /> Upload
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Recent Comments */}
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-white mb-4">Recent Comments</h2>
|
||||
<div className="bg-nova-900/40 border border-white/10 rounded-2xl p-4">
|
||||
{recentComments?.length > 0 ? (
|
||||
recentComments.map((c) => <RecentComment key={c.id} comment={c} />)
|
||||
) : (
|
||||
<p className="text-slate-500 text-sm text-center py-4">No comments yet</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-6 grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
{showWidget('module_summaries') && <section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-white">Module health</h2>
|
||||
<a href="/studio/content" className="text-sm font-medium text-sky-100">Open content queue</a>
|
||||
</div>
|
||||
<div className="mt-5 grid gap-4 md:grid-cols-2">
|
||||
{(overview.module_summaries || []).map((item) => (
|
||||
<div key={item.key} className="rounded-[24px] border border-white/10 bg-black/20 p-4">
|
||||
<div className="flex items-center gap-3 text-slate-200">
|
||||
<i className={item.icon} />
|
||||
<span className="text-base font-semibold text-white">{item.label}</span>
|
||||
</div>
|
||||
<div className="mt-4 flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-3xl font-semibold text-white">{Number(item.count || 0).toLocaleString()}</div>
|
||||
<div className="mt-2 text-sm text-slate-400">{Number(item.published_count || 0).toLocaleString()} published</div>
|
||||
<div className="mt-1 text-xs uppercase tracking-[0.18em] text-sky-200/70">{Number(item.trend_value || 0).toLocaleString()} {item.trend_label || 'recent'}</div>
|
||||
</div>
|
||||
<div className="text-right text-sm text-slate-400">
|
||||
<div>{Number(item.draft_count || 0).toLocaleString()} drafts</div>
|
||||
<div>{Number(item.archived_count || 0).toLocaleString()} archived</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
<a href={item.index_url} className="inline-flex items-center gap-2 rounded-full border border-white/10 px-3 py-1.5 text-xs text-slate-100">Manage</a>
|
||||
<a href={item.create_url} className="inline-flex items-center gap-2 rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1.5 text-xs text-sky-100">{item.quick_action_label || 'Create new'}</a>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>}
|
||||
|
||||
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-white">Quick create</h2>
|
||||
<span className="text-sm text-slate-500">Start with any module</span>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-3">
|
||||
{(overview.quick_create || []).map((item) => (
|
||||
<QuickCreateCard key={item.key} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px_360px]">
|
||||
{showWidget('active_challenges') && <section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-white">Active challenges</h2>
|
||||
<a href="/studio/challenges" onClick={() => trackStudioEvent('studio_challenge_action_taken', { surface: studioSurface(), module: 'overview', meta: { action: 'open_challenges' } })} className="text-sm font-medium text-sky-100">Open challenges</a>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-1">
|
||||
{(overview.active_challenges?.items || []).map((item) => <ChallengeWidget key={item.id} challenge={item} />)}
|
||||
</div>
|
||||
</section>}
|
||||
|
||||
{showWidget('featured_status') && <section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-white">Featured status</h2>
|
||||
<a href="/studio/featured" className="text-sm font-medium text-sky-100">Manage featured</a>
|
||||
</div>
|
||||
<div className="mt-4 rounded-[24px] border border-white/10 bg-black/20 p-4">
|
||||
<div className="flex items-end justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-3xl font-semibold text-white">{overview.featured_status?.selected_count || 0}/{overview.featured_status?.target_count || 4}</div>
|
||||
<div className="mt-1 text-sm text-slate-400">modules have a selected featured item</div>
|
||||
</div>
|
||||
<div className="text-right text-xs uppercase tracking-[0.16em] text-slate-500">{(overview.featured_status?.missing_modules || []).length} missing</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 space-y-3">
|
||||
{(overview.featured_status?.items || []).slice(0, 3).map((item) => <FeaturedStatusCard key={item.id} item={item} />)}
|
||||
</div>
|
||||
</section>}
|
||||
|
||||
{showWidget('creator_health') && <section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-white">Creator health</h2>
|
||||
<a href="/studio/growth" className="text-sm font-medium text-sky-100">Open growth</a>
|
||||
</div>
|
||||
<div className="mt-4 rounded-[24px] border border-white/10 bg-black/20 p-4">
|
||||
<div className="text-3xl font-semibold text-white">{overview.creator_health?.score || 0}</div>
|
||||
<div className="mt-1 text-sm text-slate-400">blended workflow health score</div>
|
||||
</div>
|
||||
<div className="mt-4 space-y-3">
|
||||
{(overview.creator_health?.checkpoints || []).map((item) => (
|
||||
<a key={item.key} href={item.href} className="block rounded-[24px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-white">{item.label}</h3>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-400">{item.detail}</p>
|
||||
</div>
|
||||
<span className="text-xl font-semibold text-white">{item.score}</span>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
{showWidget('continue_working') && <section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-white">Continue working</h2>
|
||||
<a href="/studio/drafts" className="text-sm font-medium text-sky-100">Open drafts</a>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-3 md:grid-cols-3">
|
||||
{(overview.continue_working || []).map((item) => <ContinueWorkingCard key={item.id} item={item} />)}
|
||||
</div>
|
||||
</section>}
|
||||
|
||||
{showWidget('scheduled_items') && <section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-white">Upcoming schedule</h2>
|
||||
<a href="/studio/scheduled" className="text-sm font-medium text-sky-100">Open calendar</a>
|
||||
</div>
|
||||
<div className="mt-4 space-y-3">
|
||||
{(overview.scheduled_items || []).slice(0, 4).map((item) => <ScheduledItemCard key={item.id} item={item} />)}
|
||||
</div>
|
||||
</section>}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-white">Top performers</h2>
|
||||
<a href="/studio/analytics" className="text-sm font-medium text-sky-100">Open insights</a>
|
||||
</div>
|
||||
{overview.top_performers?.length > 0 ? (
|
||||
<div className="mt-5 grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{overview.top_performers.map((item) => (
|
||||
<TopPerformerCard key={item.id} item={item} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-5 rounded-[24px] border border-dashed border-white/15 px-6 py-12 text-center text-slate-400">Nothing has enough activity yet to rank here.</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{showWidget('draft_reminders') && <section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-lg font-semibold text-white">Draft reminders</h2>
|
||||
<div className="mt-4 space-y-3">
|
||||
{(overview.draft_reminders || []).map((item) => (
|
||||
<a key={item.id} href={item.edit_url || item.manage_url} className="block rounded-2xl border border-white/10 bg-black/20 p-4">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/70">{item.module_label}</p>
|
||||
<h3 className="mt-2 text-base font-semibold text-white">{item.title}</h3>
|
||||
<p className="mt-1 text-sm text-slate-400">Updated {new Date(item.updated_at).toLocaleDateString()}</p>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
{showWidget('recent_activity') && <section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-white">Recent activity</h2>
|
||||
<a href="/studio/activity" className="text-sm font-medium text-sky-100">Open inbox</a>
|
||||
</div>
|
||||
<div className="mt-5 grid gap-4 lg:grid-cols-2">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold uppercase tracking-[0.18em] text-slate-500">Recent publishes</h3>
|
||||
<div className="mt-3 space-y-3">
|
||||
{(overview.recent_publishes || []).slice(0, 4).map((item) => (
|
||||
<RecentPublishCard key={item.id} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold uppercase tracking-[0.18em] text-slate-500">Recent followers</h3>
|
||||
<div className="mt-3 space-y-3">
|
||||
{(overview.recent_followers || []).map((follower) => (
|
||||
<a key={follower.id} href={follower.profile_url} className="flex items-center gap-3 rounded-2xl border border-white/10 bg-black/20 px-4 py-3">
|
||||
{follower.avatar_url ? (
|
||||
<img src={follower.avatar_url} alt={follower.username} className="h-11 w-11 rounded-2xl object-cover" />
|
||||
) : (
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-white/5 text-slate-400">
|
||||
<i className="fa-solid fa-user" />
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-semibold text-white">{follower.name}</div>
|
||||
<div className="text-xs text-slate-400">@{follower.username}</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold uppercase tracking-[0.18em] text-slate-500">Inbox feed</h3>
|
||||
<div className="mt-3 space-y-3">
|
||||
{(overview.recent_activity || []).slice(0, 4).map((item) => (
|
||||
<ActivityRow key={item.id} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>}
|
||||
|
||||
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-lg font-semibold text-white">Growth hints</h2>
|
||||
<div className="mt-4 space-y-3">
|
||||
{(overview.growth_hints || []).map((item) => (
|
||||
<GrowthHint key={item.title} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-white">Recent comments</h2>
|
||||
<a href="/studio/comments" className="text-sm font-medium text-sky-100">View all</a>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
{(overview.recent_comments || []).length > 0 ? (
|
||||
overview.recent_comments.map((comment) => <RecentComment key={comment.id} comment={comment} />)
|
||||
) : (
|
||||
<p className="py-6 text-center text-sm text-slate-500">No comments yet</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-lg font-semibold text-white">Momentum</h2>
|
||||
<div className="mt-4 space-y-4">
|
||||
{[
|
||||
['Views', analytics.totals?.views],
|
||||
['Reactions', analytics.totals?.appreciation],
|
||||
['Shares', analytics.totals?.shares],
|
||||
['Comments', analytics.totals?.comments],
|
||||
].map(([label, value]) => (
|
||||
<div key={label} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3">
|
||||
<div className="flex items-center justify-between text-sm text-slate-300">
|
||||
<span>{label}</span>
|
||||
<span className="font-semibold text-white">{Number(value || 0).toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{showWidget('stale_drafts') && <div className="mt-6 rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-white">Stale drafts</h2>
|
||||
<a href="/studio/content?bucket=drafts&stale=only&module=stories" className="text-sm font-medium text-sky-100">Filter stale work</a>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-3 md:grid-cols-4">
|
||||
{(overview.stale_drafts || []).map((item) => <ContinueWorkingCard key={item.id} item={item} />)}
|
||||
</div>
|
||||
</div>}
|
||||
</StudioLayout>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user