import React, { useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import ArtworkShareButton from './ArtworkShareButton'
function formatCount(value) {
const n = Number(value || 0)
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`
if (n >= 1_000) return `${(n / 1_000).toFixed(1).replace(/\.0$/, '')}k`
return `${n}`
}
/* ── SVG Icons ─────────────────────────────────────────────────────────────── */
function HeartIcon({ filled }) {
return filled ? (
) : (
)
}
function BookmarkIcon({ filled }) {
return filled ? (
) : (
)
}
function CloudDownIcon() {
return (
)
}
function DownloadArrowIcon() {
return (
)
}
/* ShareIcon removed — now provided by ArtworkShareButton */
function FlagIcon() {
return (
)
}
/* ── Report Modal ──────────────────────────────────────────────────────────── */
const REPORT_REASONS = [
'Inappropriate content',
'Copyright violation',
'Spam or misleading',
'Offensive or abusive',
]
function ReportModal({ open, onClose, onSubmit, submitting }) {
const [selected, setSelected] = useState('')
const [details, setDetails] = useState('')
const backdropRef = useRef(null)
const inputRef = useRef(null)
// Reset & focus when opening
useEffect(() => {
if (open) {
setSelected('')
setDetails('')
const t = setTimeout(() => inputRef.current?.focus(), 80)
return () => clearTimeout(t)
}
}, [open])
// Close on Escape
useEffect(() => {
if (!open) return
const handler = (e) => { if (e.key === 'Escape') onClose() }
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [open, onClose])
if (!open) return null
const trimmedDetails = details.trim()
const canSubmit = selected.length > 0 && trimmedDetails.length >= 10 && !submitting
const fullReason = `${selected}: ${trimmedDetails}`
return createPortal(
{ if (e.target === backdropRef.current) onClose() }}
className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"
>
{/* Header */}
{/* Body */}
{/* Step 1 — pick a reason */}
{REPORT_REASONS.map((r) => (
))}
{/* Step 2 — describe & prove */}
{/* Footer */}
,
document.body
)
}
export default function ArtworkActionBar({ artwork, stats, canonicalUrl, onStatsChange }) {
const [favorited, setFavorited] = useState(Boolean(artwork?.viewer?.is_favorited))
const [downloading, setDownloading] = useState(false)
const [reporting, setReporting] = useState(false)
const [reported, setReported] = useState(false)
const [reportOpen, setReportOpen] = useState(false)
const isLoggedIn = artwork?.viewer != null
useEffect(() => {
setFavorited(Boolean(artwork?.viewer?.is_favorited))
}, [artwork?.id, artwork?.viewer?.is_favorited])
const fallbackUrl = artwork?.thumbs?.xl?.url || artwork?.thumbs?.lg?.url || artwork?.file?.url || '#'
const shareUrl = canonicalUrl || artwork?.canonical_url || (typeof window !== 'undefined' ? window.location.href : '#')
const csrfToken = typeof document !== 'undefined'
? document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
: null
// Track view
useEffect(() => {
if (!artwork?.id) return
const key = `sb_viewed_${artwork.id}`
if (typeof sessionStorage !== 'undefined' && sessionStorage.getItem(key)) return
fetch(`/api/art/${artwork.id}/view`, {
method: 'POST',
headers: { 'X-CSRF-TOKEN': csrfToken || '', 'Content-Type': 'application/json' },
credentials: 'same-origin',
}).then(res => {
if (res.ok && typeof sessionStorage !== 'undefined') sessionStorage.setItem(key, '1')
}).catch(() => {})
}, [artwork?.id]) // eslint-disable-line react-hooks/exhaustive-deps
const postInteraction = async (url, body) => {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken || '' },
credentials: 'same-origin',
body: JSON.stringify(body),
})
if (!response.ok) throw new Error('Request failed')
return response.json()
}
const handleDownload = async () => {
if (downloading || !artwork?.id) return
setDownloading(true)
try {
const res = await fetch(`/api/art/${artwork.id}/download`, {
method: 'POST',
headers: { 'X-CSRF-TOKEN': csrfToken || '', 'Content-Type': 'application/json' },
credentials: 'same-origin',
})
const data = res.ok ? await res.json() : null
const url = data?.url || fallbackUrl
const a = document.createElement('a')
a.href = url
a.download = data?.filename || ''
a.rel = 'noopener noreferrer'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
} catch {
window.open(fallbackUrl, '_blank', 'noopener,noreferrer')
} finally {
setDownloading(false)
}
}
const onToggleFavorite = async () => {
const nextState = !favorited
setFavorited(nextState)
try {
await postInteraction(`/api/artworks/${artwork.id}/favorite`, { state: nextState })
onStatsChange?.({ favorites: nextState ? 1 : -1 })
} catch { setFavorited(!nextState) }
}
const openReport = () => {
if (reported) return
setReportOpen(true)
}
const submitReport = async (reason) => {
if (reporting) return
setReporting(true)
try {
await postInteraction(`/api/artworks/${artwork.id}/report`, { reason })
setReported(true)
setReportOpen(false)
} catch { /* noop */ }
finally { setReporting(false) }
}
const favCount = formatCount(stats?.favorites ?? artwork?.stats?.favorites ?? 0)
const viewCount = formatCount(stats?.views ?? artwork?.stats?.views ?? 0)
return (
<>
{/* ── Desktop centered bar ────────────────────────────────────── */}
{/* Favourite (heart) stat pill */}
{/* Views stat pill */}
{viewCount}
{/* Share pill */}
{/* Report pill */}
{/* Download button */}
{/* ── Mobile fixed bottom bar ─────────────────────────────────── */}
{/* Share */}
{/* Report */}
{/* Report modal */}
setReportOpen(false)}
onSubmit={submitReport}
submitting={reporting}
/>
>
)
}