Files
SkinbaseNova/resources/js/components/Studio/StudioTable.jsx
Gregor Klevze a875203482 feat: Nova UI component library + Studio dropdown/picker polish
- Add Nova UI library: Button, TextInput, Textarea, FormField, Select,
  NovaSelect, Checkbox, Radio/RadioGroup, Toggle, DatePicker,
  DateRangePicker, Modal + barrel index.js
- Replace all native <select> in Studio with NovaSelect (StudioFilters,
  StudioToolbar, BulkActionsBar) including frosted-glass portal and
  category group headers
- Replace native checkboxes in StudioGridCard, StudioTable, UploadSidebar,
  UploadWizard, Upload/Index with custom Checkbox component
- Add nova-scrollbar CSS utility (thin 4px, semi-transparent)
- Fix portal position drift: use viewport-relative coords (no scrollY offset)
  for NovaSelect, DatePicker and DateRangePicker
- Close portals on external scroll instead of remeasuring
- Improve hover highlight visibility in NovaSelect (bg-white/[0.13])
- Move search icon to right side in NovaSelect dropdown
- Reduce Studio layout top spacing (py-6 -> pt-4 pb-8)
- Add StudioCheckbox and SquareCheckbox backward-compat shims
- Add sync.sh rsync deploy script
2026-03-01 10:41:43 +01:00

144 lines
6.6 KiB
JavaScript

import React from 'react'
import StatusBadge from '../Badges/StatusBadge'
import RisingBadge from '../Badges/RisingBadge'
import Checkbox from '../ui/Checkbox'
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">
<Checkbox
checked={allSelected}
onChange={onSelectAll}
aria-label="Select all artworks"
/>
</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">
<Checkbox
checked={selectedIds.includes(art.id)}
onChange={() => onSelect(art.id)}
aria-label={`Select ${art.title}`}
/>
</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>
)
}