- 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
102 lines
3.6 KiB
JavaScript
102 lines
3.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'
|
|
}
|
|
|
|
function statItem(icon, value) {
|
|
return (
|
|
<span className="flex items-center gap-1 text-xs text-slate-400">
|
|
<span>{icon}</span>
|
|
<span>{typeof value === 'number' ? value.toLocaleString() : value}</span>
|
|
</span>
|
|
)
|
|
}
|
|
|
|
export default function StudioGridCard({ artwork, selected, onSelect, onAction }) {
|
|
const status = getStatus(artwork)
|
|
|
|
return (
|
|
<div
|
|
className={`group relative bg-nova-900/60 border rounded-2xl overflow-hidden transition-all duration-300 hover:-translate-y-0.5 hover:shadow-xl hover:shadow-accent/5 ${
|
|
selected ? 'border-accent/60 ring-2 ring-accent/20' : 'border-white/10 hover:border-white/20'
|
|
}`}
|
|
>
|
|
{/* Selection checkbox */}
|
|
<div className="absolute top-3 left-3 z-10">
|
|
<Checkbox
|
|
checked={selected}
|
|
onChange={() => onSelect(artwork.id)}
|
|
aria-label={`Select ${artwork.title}`}
|
|
/>
|
|
</div>
|
|
|
|
{/* Thumbnail */}
|
|
<div className="relative aspect-[4/3] bg-nova-800 overflow-hidden">
|
|
<img
|
|
src={artwork.thumb_url}
|
|
alt={artwork.title}
|
|
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
|
loading="lazy"
|
|
/>
|
|
|
|
{/* Hover actions */}
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
|
<div className="absolute bottom-3 right-3 flex gap-1.5">
|
|
<ActionBtn icon="fa-eye" title="View public" onClick={() => window.open(`/artworks/${artwork.slug}`, '_blank')} />
|
|
<ActionBtn icon="fa-pen" title="Edit" onClick={() => onAction('edit', artwork)} />
|
|
{status !== 'archived' ? (
|
|
<ActionBtn icon="fa-box-archive" title="Archive" onClick={() => onAction('archive', artwork)} />
|
|
) : (
|
|
<ActionBtn icon="fa-rotate-left" title="Unarchive" onClick={() => onAction('unarchive', artwork)} />
|
|
)}
|
|
<ActionBtn icon="fa-trash" title="Delete" onClick={() => onAction('delete', artwork)} danger />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Info */}
|
|
<div className="p-3 space-y-2">
|
|
<h3 className="text-sm font-semibold text-white truncate" title={artwork.title}>
|
|
{artwork.title}
|
|
</h3>
|
|
|
|
<div className="flex flex-wrap items-center gap-1.5">
|
|
<StatusBadge status={status} />
|
|
<RisingBadge heatScore={artwork.heat_score} rankingScore={artwork.ranking_score} />
|
|
</div>
|
|
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
{statItem('👁', artwork.views)}
|
|
{statItem('❤️', artwork.favourites)}
|
|
{statItem('🔗', artwork.shares)}
|
|
{statItem('💬', artwork.comments)}
|
|
{statItem('⬇', artwork.downloads)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ActionBtn({ icon, title, onClick, danger }) {
|
|
return (
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); onClick() }}
|
|
title={title}
|
|
className={`w-8 h-8 rounded-lg flex items-center justify-center text-sm transition-all backdrop-blur-sm ${
|
|
danger
|
|
? 'bg-red-500/20 text-red-400 hover:bg-red-500/40'
|
|
: 'bg-white/10 text-white hover:bg-white/20'
|
|
}`}
|
|
aria-label={title}
|
|
>
|
|
<i className={`fa-solid ${icon}`} />
|
|
</button>
|
|
)
|
|
}
|