feat: artwork share system with modal, native Web Share API, and tracking
- Add ArtworkShareModal with glassmorphism UI (Facebook, X, Pinterest, Email, Copy Link, Embed Code)
- Add ArtworkShareButton with lazy-loaded modal and native share fallback
- Add useWebShare hook abstracting navigator.share with AbortError handling
- Add ShareToast auto-dismissing notification component
- Add share() endpoint to ArtworkInteractionController (POST /api/artworks/{id}/share)
- Add artwork_shares migration for Phase 2 share tracking
- Refactor ArtworkActionBar to use new ArtworkShareButton component
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import ArtworkShareButton from './ArtworkShareButton'
|
||||
|
||||
function formatCount(value) {
|
||||
const n = Number(value || 0)
|
||||
@@ -49,13 +50,7 @@ function DownloadArrowIcon() {
|
||||
)
|
||||
}
|
||||
|
||||
function ShareIcon() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="h-5 w-5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 1 1 0-2.684m0 2.684 6.632 3.316m-6.632-6 6.632-3.316m0 0a3 3 0 1 0 5.367-2.684 3 3 0 0 0-5.367 2.684Zm0 9.316a3 3 0 1 0 5.368 2.684 3 3 0 0 0-5.368-2.684Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
/* ShareIcon removed — now provided by ArtworkShareButton */
|
||||
|
||||
function FlagIcon() {
|
||||
return (
|
||||
@@ -201,8 +196,6 @@ export default function ArtworkActionBar({ artwork, stats, canonicalUrl, onStats
|
||||
const [reporting, setReporting] = useState(false)
|
||||
const [reported, setReported] = useState(false)
|
||||
const [reportOpen, setReportOpen] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setFavorited(Boolean(artwork?.viewer?.is_favorited))
|
||||
}, [artwork?.id, artwork?.viewer?.is_favorited])
|
||||
@@ -272,18 +265,6 @@ export default function ArtworkActionBar({ artwork, stats, canonicalUrl, onStats
|
||||
} 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)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch { /* noop */ }
|
||||
}
|
||||
|
||||
const openReport = () => {
|
||||
if (reported) return
|
||||
setReportOpen(true)
|
||||
@@ -330,15 +311,7 @@ export default function ArtworkActionBar({ artwork, stats, canonicalUrl, onStats
|
||||
</div>
|
||||
|
||||
{/* Share pill */}
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Share artwork"
|
||||
onClick={onShare}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-white/[0.08] bg-white/[0.04] px-5 py-2.5 text-sm font-medium text-white/70 transition-all duration-200 hover:border-white/[0.15] hover:bg-white/[0.07] hover:text-white"
|
||||
>
|
||||
<ShareIcon />
|
||||
{copied ? 'Copied!' : 'Share'}
|
||||
</button>
|
||||
<ArtworkShareButton artwork={artwork} shareUrl={shareUrl} size="default" />
|
||||
|
||||
{/* Report pill */}
|
||||
<button
|
||||
@@ -389,14 +362,7 @@ export default function ArtworkActionBar({ artwork, stats, canonicalUrl, onStats
|
||||
</button>
|
||||
|
||||
{/* Share */}
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Share"
|
||||
onClick={onShare}
|
||||
className="inline-flex items-center gap-1.5 rounded-full border border-white/[0.08] bg-white/[0.04] px-3.5 py-2 text-xs font-medium text-white/70 transition-all"
|
||||
>
|
||||
<ShareIcon />
|
||||
</button>
|
||||
<ArtworkShareButton artwork={artwork} shareUrl={shareUrl} size="small" />
|
||||
|
||||
{/* Report */}
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user