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:
77
resources/js/components/artwork/ArtworkShareButton.jsx
Normal file
77
resources/js/components/artwork/ArtworkShareButton.jsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React, { lazy, Suspense, useCallback, useState } from 'react'
|
||||
import useWebShare from '../../hooks/useWebShare'
|
||||
|
||||
const ArtworkShareModal = lazy(() => import('./ArtworkShareModal'))
|
||||
|
||||
/* ── Share icon (lucide-style) ───────────────────────────────────────────── */
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* ArtworkShareButton – renders the Share pill and manages modal / native share.
|
||||
*
|
||||
* Props:
|
||||
* artwork – artwork object
|
||||
* shareUrl – canonical URL to share
|
||||
* size – 'default' | 'small' (for mobile bar)
|
||||
*/
|
||||
export default function ArtworkShareButton({ artwork, shareUrl, size = 'default' }) {
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
|
||||
const openModal = useCallback(
|
||||
() => setModalOpen(true),
|
||||
[],
|
||||
)
|
||||
const closeModal = useCallback(
|
||||
() => setModalOpen(false),
|
||||
[],
|
||||
)
|
||||
|
||||
const { share } = useWebShare({ onFallback: openModal })
|
||||
|
||||
const handleClick = () => {
|
||||
share({
|
||||
title: artwork?.title || 'Artwork',
|
||||
text: artwork?.description?.substring(0, 120) || '',
|
||||
url: shareUrl || artwork?.canonical_url || window.location.href,
|
||||
})
|
||||
}
|
||||
|
||||
const isSmall = size === 'small'
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Share artwork"
|
||||
onClick={handleClick}
|
||||
className={
|
||||
isSmall
|
||||
? '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 hover:border-white/[0.15] hover:bg-white/[0.07] hover:text-white'
|
||||
: 'group 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 hover:shadow-lg hover:shadow-white/[0.03]'
|
||||
}
|
||||
title="Share"
|
||||
>
|
||||
<ShareIcon />
|
||||
{!isSmall && <span>Share</span>}
|
||||
</button>
|
||||
|
||||
{/* Lazy-loaded modal – only rendered when opened */}
|
||||
{modalOpen && (
|
||||
<Suspense fallback={null}>
|
||||
<ArtworkShareModal
|
||||
open={modalOpen}
|
||||
onClose={closeModal}
|
||||
artwork={artwork}
|
||||
shareUrl={shareUrl}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user