Studio: make grid checkbox rectangular and commit table changes
This commit is contained in:
213
resources/js/Pages/Studio/StudioAnalytics.jsx
Normal file
213
resources/js/Pages/Studio/StudioAnalytics.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user