Implement creator studio and upload updates
This commit is contained in:
@@ -1,48 +1,119 @@
|
||||
import React from 'react'
|
||||
import { usePage, Link } from '@inertiajs/react'
|
||||
import { router, usePage } from '@inertiajs/react'
|
||||
import StudioLayout from '../../Layouts/StudioLayout'
|
||||
import { studioSurface, trackStudioEvent } from '../../utils/studioEvents'
|
||||
|
||||
const kpiItems = [
|
||||
{ key: 'views', label: 'Total Views', icon: 'fa-eye', color: 'text-emerald-400', bg: 'bg-emerald-500/10' },
|
||||
{ key: 'favourites', label: 'Total Favourites', icon: 'fa-heart', color: 'text-pink-400', bg: 'bg-pink-500/10' },
|
||||
{ key: 'shares', label: 'Total Shares', icon: 'fa-share-nodes', color: 'text-amber-400', bg: 'bg-amber-500/10' },
|
||||
{ key: 'downloads', label: 'Total Downloads', icon: 'fa-download', color: 'text-purple-400', bg: 'bg-purple-500/10' },
|
||||
{ key: 'comments', label: 'Total Comments', icon: 'fa-comment', color: 'text-blue-400', bg: 'bg-blue-500/10' },
|
||||
{ key: 'views', label: 'Views', icon: 'fa-eye', color: 'text-emerald-400', bg: 'bg-emerald-500/10' },
|
||||
{ key: 'appreciation', label: 'Reactions', icon: 'fa-heart', color: 'text-pink-400', bg: 'bg-pink-500/10' },
|
||||
{ key: 'shares', label: 'Shares', icon: 'fa-share-nodes', color: 'text-amber-400', bg: 'bg-amber-500/10' },
|
||||
{ key: 'saves', label: 'Saves', icon: 'fa-bookmark', color: 'text-purple-400', bg: 'bg-purple-500/10' },
|
||||
{ key: 'comments', label: 'Comments', icon: 'fa-comment', color: 'text-blue-400', bg: 'bg-blue-500/10' },
|
||||
{ key: 'followers', label: 'Followers', icon: 'fa-user-group', color: 'text-cyan-300', bg: 'bg-cyan-400/10' },
|
||||
]
|
||||
|
||||
const performanceItems = [
|
||||
{ key: 'avg_ranking', label: 'Avg Ranking Score', icon: 'fa-trophy', color: 'text-yellow-400', bg: 'bg-yellow-500/10' },
|
||||
{ key: 'avg_heat', label: 'Avg Heat Score', icon: 'fa-fire', color: 'text-orange-400', bg: 'bg-orange-500/10' },
|
||||
]
|
||||
const rangeOptions = [7, 14, 30, 60, 90]
|
||||
|
||||
const contentTypeIcons = {
|
||||
skins: 'fa-layer-group',
|
||||
wallpapers: 'fa-desktop',
|
||||
photography: 'fa-camera',
|
||||
other: 'fa-folder-open',
|
||||
members: 'fa-users',
|
||||
function formatShortDate(value) {
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
|
||||
}
|
||||
|
||||
const contentTypeColors = {
|
||||
skins: 'text-emerald-400 bg-emerald-500/10',
|
||||
wallpapers: 'text-blue-400 bg-blue-500/10',
|
||||
photography: 'text-amber-400 bg-amber-500/10',
|
||||
other: 'text-slate-400 bg-slate-500/10',
|
||||
members: 'text-purple-400 bg-purple-500/10',
|
||||
function TrendChart({ title, subtitle, points, colorClass, fillClass, icon }) {
|
||||
const values = (points || []).map((point) => Number(point.value || 0))
|
||||
const maxValue = Math.max(...values, 1)
|
||||
|
||||
return (
|
||||
<section className="rounded-[28px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">{title}</h2>
|
||||
<p className="mt-1 text-sm text-slate-400">{subtitle}</p>
|
||||
</div>
|
||||
<div className={`flex h-11 w-11 items-center justify-center rounded-2xl ${fillClass} ${colorClass}`}>
|
||||
<i className={`fa-solid ${icon}`} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex h-52 items-end gap-2">
|
||||
{(points || []).map((point) => {
|
||||
const height = `${Math.max(8, Math.round((Number(point.value || 0) / maxValue) * 100))}%`
|
||||
|
||||
return (
|
||||
<div key={point.date} className="flex min-w-0 flex-1 flex-col items-center justify-end gap-2">
|
||||
<div className="text-[10px] font-medium text-slate-500">{Number(point.value || 0).toLocaleString()}</div>
|
||||
<div className="flex h-full w-full items-end rounded-t-[18px] bg-white/[0.03] px-[2px]">
|
||||
<div className={`w-full rounded-t-[16px] ${fillClass}`} style={{ height }} />
|
||||
</div>
|
||||
<div className="text-[10px] uppercase tracking-[0.14em] text-slate-500">{formatShortDate(point.date)}</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default function StudioAnalytics() {
|
||||
const { props } = usePage()
|
||||
const { totals, topArtworks, contentBreakdown, recentComments } = props
|
||||
const {
|
||||
totals,
|
||||
topContent,
|
||||
moduleBreakdown,
|
||||
recentComments,
|
||||
publishingTimeline,
|
||||
viewsTrend,
|
||||
engagementTrend,
|
||||
comparison,
|
||||
insightBlocks,
|
||||
rangeDays,
|
||||
} = props
|
||||
|
||||
const totalArtworksCount = (contentBreakdown || []).reduce((sum, ct) => sum + ct.count, 0)
|
||||
const updateRange = (days) => {
|
||||
trackStudioEvent('studio_filter_used', {
|
||||
surface: studioSurface(),
|
||||
module: 'analytics',
|
||||
meta: {
|
||||
range_days: days,
|
||||
},
|
||||
})
|
||||
|
||||
router.get(window.location.pathname, { range_days: days }, {
|
||||
preserveScroll: true,
|
||||
preserveState: true,
|
||||
replace: true,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<StudioLayout title="Analytics">
|
||||
{/* KPI Cards */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4 mb-8">
|
||||
<StudioLayout title="Analytics" subtitle="Cross-module insights for the whole creator workspace, not just artwork uploads.">
|
||||
<section className="mb-6 rounded-[28px] border border-white/10 bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.14),_transparent_35%),radial-gradient(circle_at_bottom_right,_rgba(244,114,182,0.12),_transparent_40%),linear-gradient(135deg,_rgba(15,23,42,0.86),_rgba(2,6,23,0.96))] p-5 shadow-[0_22px_60px_rgba(2,6,23,0.28)]">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-500">Analytics window</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold text-white">Performance over the last {rangeDays || 30} days</h2>
|
||||
<p className="mt-2 max-w-2xl text-sm leading-6 text-slate-400">This view compares module output, shows views and engagement trends over time, and keeps publishing rhythm in the same window.</p>
|
||||
</div>
|
||||
|
||||
<div className="inline-flex rounded-full border border-white/10 bg-black/20 p-1">
|
||||
{rangeOptions.map((days) => (
|
||||
<button
|
||||
key={days}
|
||||
type="button"
|
||||
onClick={() => updateRange(days)}
|
||||
className={`rounded-full px-4 py-2 text-sm font-semibold transition ${Number(rangeDays || 30) === days ? 'bg-white text-slate-950' : 'text-slate-300 hover:text-white'}`}
|
||||
>
|
||||
{days}d
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 xl:grid-cols-6">
|
||||
{kpiItems.map((item) => (
|
||||
<div key={item.key} className="bg-nova-900/60 border border-white/10 rounded-2xl p-5 hover:border-white/20 transition-all">
|
||||
<div key={item.key} className="rounded-[26px] border border-white/10 bg-white/[0.03] p-5">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className={`w-10 h-10 rounded-xl ${item.bg} flex items-center justify-center ${item.color}`}>
|
||||
<i className={`fa-solid ${item.icon}`} />
|
||||
@@ -56,157 +127,184 @@ export default function StudioAnalytics() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Performance Averages */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
|
||||
{performanceItems.map((item) => (
|
||||
<div key={item.key} className="bg-nova-900/60 border border-white/10 rounded-2xl p-5 hover:border-white/20 transition-all">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className={`w-10 h-10 rounded-xl ${item.bg} flex items-center justify-center ${item.color}`}>
|
||||
<i className={`fa-solid ${item.icon} text-lg`} />
|
||||
</div>
|
||||
<span className="text-xs font-medium text-slate-400 uppercase tracking-wider">{item.label}</span>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-white tabular-nums">
|
||||
{(totals?.[item.key] ?? 0).toFixed(1)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-6 grid gap-6 xl:grid-cols-2">
|
||||
<TrendChart
|
||||
title="Views over time"
|
||||
subtitle="Cross-module reach across the current analytics window."
|
||||
points={viewsTrend}
|
||||
colorClass="text-emerald-300"
|
||||
fillClass="bg-emerald-400/60"
|
||||
icon="fa-eye"
|
||||
/>
|
||||
<TrendChart
|
||||
title="Engagement over time"
|
||||
subtitle="Combined engagement score so you can see momentum shifts, not just raw traffic."
|
||||
points={engagementTrend}
|
||||
colorClass="text-pink-300"
|
||||
fillClass="bg-pink-400/60"
|
||||
icon="fa-bolt"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||
{/* Content Breakdown */}
|
||||
<div className="bg-nova-900/60 border border-white/10 rounded-2xl p-6">
|
||||
<h3 className="text-sm font-semibold text-white mb-4">
|
||||
<i className="fa-solid fa-chart-pie text-slate-500 mr-2" />
|
||||
Content Breakdown
|
||||
</h3>
|
||||
{contentBreakdown?.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{contentBreakdown.map((ct) => {
|
||||
const pct = totalArtworksCount > 0 ? Math.round((ct.count / totalArtworksCount) * 100) : 0
|
||||
const iconClass = contentTypeIcons[ct.slug] || 'fa-folder'
|
||||
const colorClass = contentTypeColors[ct.slug] || 'text-slate-400 bg-slate-500/10'
|
||||
const [textColor, bgColor] = colorClass.split(' ')
|
||||
return (
|
||||
<div key={ct.slug} className="flex items-center gap-3">
|
||||
<div className={`w-8 h-8 rounded-lg ${bgColor} flex items-center justify-center ${textColor} flex-shrink-0`}>
|
||||
<i className={`fa-solid ${iconClass} text-xs`} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm text-white">{ct.name}</span>
|
||||
<span className="text-xs text-slate-400 tabular-nums">{ct.count}</span>
|
||||
</div>
|
||||
<div className="h-1.5 rounded-full bg-white/5 overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full ${bgColor.replace('/10', '/40')}`}
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6 grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<section className="rounded-[28px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-lg font-semibold text-white">Module breakdown</h2>
|
||||
<div className="mt-5 space-y-3">
|
||||
{(moduleBreakdown || []).map((item) => (
|
||||
<div key={item.key} className="rounded-[22px] border border-white/10 bg-black/20 p-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3 text-slate-200">
|
||||
<i className={item.icon} />
|
||||
<div>
|
||||
<div className="font-semibold text-white">{item.label}</div>
|
||||
<div className="text-xs text-slate-400">{Number(item.count || 0).toLocaleString()} items</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-slate-500 text-center py-6">No artworks categorised yet</p>
|
||||
)}
|
||||
</div>
|
||||
<a href={item.index_url} className="text-xs font-semibold uppercase tracking-[0.18em] text-sky-100">Open</a>
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-2 gap-3 text-sm text-slate-400 md:grid-cols-4">
|
||||
<div><div>Views</div><div className="mt-1 font-semibold text-white">{Number(item.views || 0).toLocaleString()}</div></div>
|
||||
<div><div>Reactions</div><div className="mt-1 font-semibold text-white">{Number(item.appreciation || 0).toLocaleString()}</div></div>
|
||||
<div><div>Comments</div><div className="mt-1 font-semibold text-white">{Number(item.comments || 0).toLocaleString()}</div></div>
|
||||
<div><div>Shares</div><div className="mt-1 font-semibold text-white">{Number(item.shares || 0).toLocaleString()}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Recent Comments */}
|
||||
<div className="lg:col-span-2 bg-nova-900/60 border border-white/10 rounded-2xl p-6">
|
||||
<h3 className="text-sm font-semibold text-white mb-4">
|
||||
<i className="fa-solid fa-comments text-slate-500 mr-2" />
|
||||
Recent Comments
|
||||
</h3>
|
||||
{recentComments?.length > 0 ? (
|
||||
<div className="space-y-0 divide-y divide-white/5">
|
||||
{recentComments.map((c) => (
|
||||
<div key={c.id} className="flex items-start gap-3 py-3 first:pt-0 last:pb-0">
|
||||
<div className="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center text-xs text-slate-500 flex-shrink-0">
|
||||
<i className="fa-solid fa-user" />
|
||||
<section className="rounded-[28px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-lg font-semibold text-white">Publishing rhythm</h2>
|
||||
<div className="mt-5 space-y-3">
|
||||
{(publishingTimeline || []).map((point) => (
|
||||
<div key={point.date}>
|
||||
<div className="mb-1 flex items-center justify-between text-xs text-slate-400">
|
||||
<span>{new Date(point.date).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}</span>
|
||||
<span>{point.count}</span>
|
||||
</div>
|
||||
<div className="h-2 overflow-hidden rounded-full bg-white/5">
|
||||
<div className="h-full rounded-full bg-sky-300/60" style={{ width: `${Math.min(100, point.count * 18)}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<section className="rounded-[28px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<h2 className="text-lg font-semibold text-white">Module comparison</h2>
|
||||
<span className="text-xs uppercase tracking-[0.18em] text-slate-500">Last {rangeDays || 30} days</span>
|
||||
</div>
|
||||
<div className="mt-5 space-y-4">
|
||||
{(comparison || []).map((item) => {
|
||||
const viewMax = Math.max(...(comparison || []).map((entry) => Number(entry.views || 0)), 1)
|
||||
const engagementMax = Math.max(...(comparison || []).map((entry) => Number(entry.engagement || 0)), 1)
|
||||
|
||||
return (
|
||||
<div key={item.key} className="rounded-[22px] border border-white/10 bg-black/20 p-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3 text-slate-200">
|
||||
<i className={item.icon} />
|
||||
<div>
|
||||
<div className="font-semibold text-white">{item.label}</div>
|
||||
<div className="text-xs text-slate-400">{Number(item.published_count || 0).toLocaleString()} published</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href={moduleBreakdown?.find((entry) => entry.key === item.key)?.index_url} className="text-xs font-semibold uppercase tracking-[0.18em] text-sky-100">Open</a>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm text-white">
|
||||
<span className="font-medium text-accent">{c.author_name}</span>
|
||||
{' '}on{' '}
|
||||
<span className="text-slate-300">{c.artwork_title}</span>
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-0.5 line-clamp-2">{c.body}</p>
|
||||
<p className="text-[10px] text-slate-600 mt-1">{new Date(c.created_at).toLocaleDateString()}</p>
|
||||
|
||||
<div className="mt-4 grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-xs text-slate-400">
|
||||
<span>Views</span>
|
||||
<span>{Number(item.views || 0).toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="mt-2 h-2 overflow-hidden rounded-full bg-white/5">
|
||||
<div className="h-full rounded-full bg-emerald-400/60" style={{ width: `${Math.max(4, Math.round((Number(item.views || 0) / viewMax) * 100))}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-xs text-slate-400">
|
||||
<span>Engagement</span>
|
||||
<span>{Number(item.engagement || 0).toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="mt-2 h-2 overflow-hidden rounded-full bg-white/5">
|
||||
<div className="h-full rounded-full bg-pink-400/60" style={{ width: `${Math.max(4, Math.round((Number(item.engagement || 0) / engagementMax) * 100))}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-slate-500 text-center py-6">No comments yet</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[28px] 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 text-sm text-slate-400">
|
||||
{(insightBlocks || []).map((item) => (
|
||||
<a key={item.key} href={item.href} className="block rounded-[22px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-2xl bg-white/[0.04] text-sky-100">
|
||||
<i className={item.icon} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-white">{item.title}</h3>
|
||||
<p className="mt-2 leading-6 text-slate-400">{item.body}</p>
|
||||
<span className="mt-3 inline-flex items-center gap-2 text-sm font-medium text-sky-100">{item.cta}<i className="fa-solid fa-arrow-right" /></span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Top Performers Table */}
|
||||
<div className="bg-nova-900/60 border border-white/10 rounded-2xl p-6">
|
||||
<h3 className="text-sm font-semibold text-white mb-4">
|
||||
<i className="fa-solid fa-ranking-star text-slate-500 mr-2" />
|
||||
Top 10 Artworks
|
||||
</h3>
|
||||
{topArtworks?.length > 0 ? (
|
||||
<div className="overflow-x-auto sb-scrollbar">
|
||||
<div className="mt-6 grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<section className="rounded-[28px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-lg font-semibold text-white">Top content</h2>
|
||||
<div className="mt-5 overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-left text-[11px] uppercase tracking-wider text-slate-500 border-b border-white/5">
|
||||
<th className="pb-3 pr-4">#</th>
|
||||
<th className="pb-3 pr-4">Artwork</th>
|
||||
<tr className="border-b border-white/5 text-left text-[11px] uppercase tracking-[0.18em] text-slate-500">
|
||||
<th className="pb-3 pr-4">Module</th>
|
||||
<th className="pb-3 pr-4">Title</th>
|
||||
<th className="pb-3 pr-4 text-right">Views</th>
|
||||
<th className="pb-3 pr-4 text-right">Favs</th>
|
||||
<th className="pb-3 pr-4 text-right">Shares</th>
|
||||
<th className="pb-3 pr-4 text-right">Downloads</th>
|
||||
<th className="pb-3 pr-4 text-right">Ranking</th>
|
||||
<th className="pb-3 text-right">Heat</th>
|
||||
<th className="pb-3 pr-4 text-right">Reactions</th>
|
||||
<th className="pb-3 pr-4 text-right">Comments</th>
|
||||
<th className="pb-3 text-right">Open</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/5">
|
||||
{topArtworks.map((art, i) => (
|
||||
<tr key={art.id} className="hover:bg-white/[0.02] transition-colors">
|
||||
<td className="py-3 pr-4 text-slate-500 tabular-nums">{i + 1}</td>
|
||||
<td className="py-3 pr-4">
|
||||
<Link
|
||||
href={`/studio/artworks/${art.id}/analytics`}
|
||||
className="flex items-center gap-3 group"
|
||||
>
|
||||
{art.thumb_url && (
|
||||
<img
|
||||
src={art.thumb_url}
|
||||
alt={art.title}
|
||||
className="w-9 h-9 rounded-lg object-cover bg-nova-800 flex-shrink-0 group-hover:ring-2 ring-accent/50 transition-all"
|
||||
/>
|
||||
)}
|
||||
<span className="text-white font-medium truncate max-w-[200px] group-hover:text-accent transition-colors">
|
||||
{art.title}
|
||||
</span>
|
||||
</Link>
|
||||
</td>
|
||||
<td className="py-3 pr-4 text-right text-slate-300 tabular-nums">{art.views.toLocaleString()}</td>
|
||||
<td className="py-3 pr-4 text-right text-slate-300 tabular-nums">{art.favourites.toLocaleString()}</td>
|
||||
<td className="py-3 pr-4 text-right text-slate-300 tabular-nums">{art.shares.toLocaleString()}</td>
|
||||
<td className="py-3 pr-4 text-right text-slate-300 tabular-nums">{art.downloads.toLocaleString()}</td>
|
||||
<td className="py-3 pr-4 text-right text-yellow-400 tabular-nums font-medium">{art.ranking_score.toFixed(1)}</td>
|
||||
<td className="py-3 text-right tabular-nums">
|
||||
<span className={`font-medium ${art.heat_score > 5 ? 'text-orange-400' : 'text-slate-400'}`}>
|
||||
{art.heat_score.toFixed(1)}
|
||||
</span>
|
||||
{art.heat_score > 5 && (
|
||||
<i className="fa-solid fa-fire text-orange-400 ml-1 text-[10px]" />
|
||||
)}
|
||||
</td>
|
||||
{(topContent || []).map((item) => (
|
||||
<tr key={item.id}>
|
||||
<td className="py-3 pr-4 text-slate-300">{item.module_label}</td>
|
||||
<td className="py-3 pr-4 text-white">{item.title}</td>
|
||||
<td className="py-3 pr-4 text-right text-slate-300">{Number(item.metrics?.views || 0).toLocaleString()}</td>
|
||||
<td className="py-3 pr-4 text-right text-slate-300">{Number(item.metrics?.appreciation || 0).toLocaleString()}</td>
|
||||
<td className="py-3 pr-4 text-right text-slate-300">{Number(item.metrics?.comments || 0).toLocaleString()}</td>
|
||||
<td className="py-3 text-right"><a href={item.analytics_url || item.view_url} className="text-sky-100">Open</a></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-slate-500 text-center py-8">No published artworks with stats yet</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="rounded-[28px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-lg font-semibold text-white">Recent comments</h2>
|
||||
<div className="mt-4 space-y-3">
|
||||
{(recentComments || []).map((comment) => (
|
||||
<article key={comment.id} className="rounded-[22px] border border-white/10 bg-black/20 p-4">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/70">{comment.module_label}</p>
|
||||
<p className="mt-2 text-sm text-white">{comment.author_name} on {comment.item_title}</p>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-400">{comment.body}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</StudioLayout>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user