Files
SkinbaseNova/resources/js/components/artwork/ArtworkActions.jsx
2026-02-27 09:46:51 +01:00

162 lines
5.3 KiB
JavaScript

import React, { useState, useEffect } from 'react'
export default function ArtworkActions({ artwork, canonicalUrl, mobilePriority = false }) {
const [liked, setLiked] = useState(Boolean(artwork?.viewer?.is_liked))
const [favorited, setFavorited] = useState(Boolean(artwork?.viewer?.is_favorited))
const [reporting, setReporting] = useState(false)
const downloadUrl = 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 the view once per browser session (sessionStorage prevents re-firing).
useEffect(() => {
if (!artwork?.id) return
const key = `sb_viewed_${artwork.id}`
if (typeof sessionStorage !== 'undefined' && sessionStorage.getItem(key)) return
if (typeof sessionStorage !== 'undefined') sessionStorage.setItem(key, '1')
fetch(`/api/art/${artwork.id}/view`, {
method: 'POST',
headers: { 'X-CSRF-TOKEN': csrfToken || '', 'Content-Type': 'application/json' },
credentials: 'same-origin',
}).catch(() => {})
}, [artwork?.id]) // eslint-disable-line react-hooks/exhaustive-deps
// Fire-and-forget download tracking — does not interrupt the native download.
const trackDownload = () => {
if (!artwork?.id) return
fetch(`/api/art/${artwork.id}/download`, {
method: 'POST',
headers: { 'X-CSRF-TOKEN': csrfToken || '', 'Content-Type': 'application/json' },
credentials: 'same-origin',
}).catch(() => {})
}
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 onToggleLike = async () => {
const nextState = !liked
setLiked(nextState)
try {
await postInteraction(`/api/artworks/${artwork.id}/like`, { state: nextState })
} catch {
setLiked(!nextState)
}
}
const onToggleFavorite = async () => {
const nextState = !favorited
setFavorited(nextState)
try {
await postInteraction(`/api/artworks/${artwork.id}/favorite`, { state: nextState })
} catch {
setFavorited(!nextState)
}
}
const onShare = async () => {
try {
if (navigator.share) {
await navigator.share({
title: artwork?.title || 'Artwork',
url: shareUrl,
})
return
}
await navigator.clipboard.writeText(shareUrl)
} catch {
// noop
}
}
const onReport = async () => {
if (reporting) return
setReporting(true)
try {
await postInteraction(`/api/artworks/${artwork.id}/report`, {
reason: 'Reported from artwork page',
})
} catch {
// noop
} finally {
setReporting(false)
}
}
return (
<div className="rounded-xl border border-nova-700 bg-panel p-5 shadow-lg shadow-deep/30">
<h2 className="text-xs font-semibold uppercase tracking-wide text-soft">Actions</h2>
<div className="mt-4 flex flex-col gap-3">
<a
href={downloadUrl}
className="inline-flex min-h-11 w-full items-center justify-center rounded-lg bg-accent px-4 py-3 text-sm font-semibold text-deep hover:brightness-110"
onClick={trackDownload}
download
>
Download
</a>
<button
type="button"
className="inline-flex min-h-11 w-full items-center justify-center rounded-lg border border-nova-600 px-4 py-3 text-sm text-white hover:bg-nova-800"
onClick={onToggleLike}
>
{liked ? 'Liked' : 'Like'}
</button>
<button
type="button"
className="inline-flex min-h-11 w-full items-center justify-center rounded-lg border border-nova-600 px-4 py-3 text-sm text-white hover:bg-nova-800"
onClick={onToggleFavorite}
>
{favorited ? 'Saved' : 'Favorite'}
</button>
<button
type="button"
className="inline-flex min-h-11 w-full items-center justify-center rounded-lg border border-nova-600 px-4 py-3 text-sm text-white hover:bg-nova-800"
onClick={onShare}
>
Share
</button>
<button
type="button"
className="inline-flex min-h-11 w-full items-center justify-center rounded-lg border border-nova-600 px-4 py-3 text-sm text-white hover:bg-nova-800"
onClick={onReport}
>
{reporting ? 'Reporting…' : 'Report'}
</button>
</div>
{mobilePriority && (
<div className="pointer-events-none fixed inset-x-0 bottom-0 z-50 border-t border-nova-700 bg-panel/95 p-3 backdrop-blur lg:hidden">
<a
href={downloadUrl}
download
onClick={trackDownload}
className="pointer-events-auto inline-flex min-h-12 w-full items-center justify-center rounded-lg bg-accent px-4 py-3 text-sm font-semibold text-deep hover:brightness-110"
>
Download
</a>
</div>
)}
</div>
)
}