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,144 @@
import React from 'react'
import StatusBadge from '../Badges/StatusBadge'
import RisingBadge from '../Badges/RisingBadge'
function getStatus(art) {
if (art.deleted_at) return 'archived'
if (!art.is_public) return 'draft'
return 'published'
}
export default function StudioTable({ artworks, selectedIds, onSelect, onSelectAll, onAction, onSort, currentSort }) {
const allSelected = artworks.length > 0 && artworks.every((a) => selectedIds.includes(a.id))
const columns = [
{ key: 'title', label: 'Title', sortable: false },
{ key: 'status', label: 'Status', sortable: false },
{ key: 'category', label: 'Category', sortable: false },
{ key: 'created_at', label: 'Created', sortable: true, sort: 'created_at' },
{ key: 'views', label: 'Views', sortable: true, sort: 'views' },
{ key: 'favourites', label: 'Favs', sortable: true, sort: 'favorites_count' },
{ key: 'shares', label: 'Shares', sortable: true, sort: 'shares_count' },
{ key: 'comments', label: 'Comments', sortable: true, sort: 'comments_count' },
{ key: 'downloads', label: 'Downloads', sortable: true, sort: 'downloads' },
{ key: 'ranking_score', label: 'Rank', sortable: true, sort: 'ranking_score' },
{ key: 'heat_score', label: 'Heat', sortable: true, sort: 'heat_score' },
]
const handleSort = (col) => {
if (!col.sortable) return
const field = col.sort
const [currentField, currentDir] = (currentSort || '').split(':')
const dir = currentField === field && currentDir === 'desc' ? 'asc' : 'desc'
onSort(`${field}:${dir}`)
}
const getSortIcon = (col) => {
if (!col.sortable) return null
const [currentField, currentDir] = (currentSort || '').split(':')
if (currentField !== col.sort) return <i className="fa-solid fa-sort text-slate-600 ml-1 text-[10px]" />
return <i className={`fa-solid fa-sort-${currentDir === 'asc' ? 'up' : 'down'} text-accent ml-1 text-[10px]`} />
}
return (
<div className="overflow-x-auto rounded-2xl border border-white/10 bg-nova-900/40">
<table className="w-full text-sm text-left">
<thead className="sticky top-0 z-10 bg-nova-900/90 backdrop-blur-sm border-b border-white/10">
<tr>
<th className="p-3 w-10">
<input
type="checkbox"
checked={allSelected}
onChange={onSelectAll}
className="w-4 h-4 rounded-sm bg-transparent border border-white/20 accent-accent focus:ring-accent/50 cursor-pointer"
/>
</th>
<th className="p-3 w-12"></th>
{columns.map((col) => (
<th
key={col.key}
className={`p-3 text-xs font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap ${col.sortable ? 'cursor-pointer hover:text-white select-none' : ''}`}
onClick={() => handleSort(col)}
>
{col.label}
{getSortIcon(col)}
</th>
))}
<th className="p-3 w-20 text-xs font-semibold text-slate-400 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-white/5">
{artworks.map((art) => (
<tr
key={art.id}
className={`transition-colors ${selectedIds.includes(art.id) ? 'bg-accent/5' : 'hover:bg-white/[0.02]'}`}
>
<td className="p-3">
<input
type="checkbox"
checked={selectedIds.includes(art.id)}
onChange={() => onSelect(art.id)}
className="w-4 h-4 rounded-sm bg-transparent border border-white/20 accent-accent focus:ring-accent/50 cursor-pointer"
/>
</td>
<td className="p-3">
<img
src={art.thumb_url}
alt=""
className="w-10 h-10 rounded-lg object-cover bg-nova-800"
loading="lazy"
/>
</td>
<td className="p-3">
<span className="text-white font-medium truncate block max-w-[200px]" title={art.title}>{art.title}</span>
</td>
<td className="p-3"><StatusBadge status={getStatus(art)} /></td>
<td className="p-3 text-slate-400">{art.category || '—'}</td>
<td className="p-3 text-slate-400 whitespace-nowrap">{art.created_at ? new Date(art.created_at).toLocaleDateString() : '—'}</td>
<td className="p-3 text-slate-300 tabular-nums">{art.views.toLocaleString()}</td>
<td className="p-3 text-slate-300 tabular-nums">{art.favourites.toLocaleString()}</td>
<td className="p-3 text-slate-300 tabular-nums">{art.shares.toLocaleString()}</td>
<td className="p-3 text-slate-300 tabular-nums">{art.comments.toLocaleString()}</td>
<td className="p-3 text-slate-300 tabular-nums">{art.downloads.toLocaleString()}</td>
<td className="p-3">
<RisingBadge heatScore={0} rankingScore={art.ranking_score} />
<span className="text-slate-400 text-xs">{art.ranking_score.toFixed(1)}</span>
</td>
<td className="p-3">
<RisingBadge heatScore={art.heat_score} rankingScore={0} />
<span className="text-slate-400 text-xs">{art.heat_score.toFixed(1)}</span>
</td>
<td className="p-3">
<div className="flex items-center gap-1">
<button
onClick={() => onAction('edit', art)}
className="w-7 h-7 rounded-lg flex items-center justify-center text-xs text-slate-400 hover:text-white hover:bg-white/10 transition-all"
title="Edit"
aria-label={`Edit ${art.title}`}
>
<i className="fa-solid fa-pen" />
</button>
<button
onClick={() => onAction('delete', art)}
className="w-7 h-7 rounded-lg flex items-center justify-center text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 transition-all"
title="Delete"
aria-label={`Delete ${art.title}`}
>
<i className="fa-solid fa-trash" />
</button>
</div>
</td>
</tr>
))}
{artworks.length === 0 && (
<tr>
<td colSpan={14} className="p-12 text-center text-slate-500">
No artworks found
</td>
</tr>
)}
</tbody>
</table>
</div>
)
}