Studio: make grid checkbox rectangular and commit table changes

This commit is contained in:
2026-03-01 08:43:48 +01:00
parent 211dc58884
commit e3ca845a6d
89 changed files with 7323 additions and 475 deletions

View File

@@ -0,0 +1,213 @@
import React from 'react'
import { usePage, Link } from '@inertiajs/react'
import StudioLayout from '../../Layouts/StudioLayout'
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' },
]
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 contentTypeIcons = {
skins: 'fa-layer-group',
wallpapers: 'fa-desktop',
photography: 'fa-camera',
other: 'fa-folder-open',
members: 'fa-users',
}
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',
}
export default function StudioAnalytics() {
const { props } = usePage()
const { totals, topArtworks, contentBreakdown, recentComments } = props
const totalArtworksCount = (contentBreakdown || []).reduce((sum, ct) => sum + ct.count, 0)
return (
<StudioLayout title="Analytics">
{/* KPI Cards */}
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4 mb-8">
{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 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}`} />
</div>
<span className="text-[11px] font-medium text-slate-400 uppercase tracking-wider leading-tight">{item.label}</span>
</div>
<p className="text-3xl font-bold text-white tabular-nums">
{(totals?.[item.key] ?? 0).toLocaleString()}
</p>
</div>
))}
</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>
<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>
</div>
)
})}
</div>
) : (
<p className="text-sm text-slate-500 text-center py-6">No artworks categorised yet</p>
)}
</div>
{/* 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" />
</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>
</div>
))}
</div>
) : (
<p className="text-sm text-slate-500 text-center py-6">No comments yet</p>
)}
</div>
</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">
<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>
<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>
</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>
</tr>
))}
</tbody>
</table>
</div>
) : (
<p className="text-sm text-slate-500 text-center py-8">No published artworks with stats yet</p>
)}
</div>
</StudioLayout>
)
}