- 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
56 lines
1.9 KiB
JavaScript
56 lines
1.9 KiB
JavaScript
import React, { useEffect, useState } from 'react'
|
||
import { createPortal } from 'react-dom'
|
||
|
||
/**
|
||
* ShareToast — a minimal, auto-dismissing toast notification.
|
||
*
|
||
* Props:
|
||
* message – text to display
|
||
* visible – whether the toast is currently shown
|
||
* onHide – callback when the toast finishes (auto-hidden after ~2 s)
|
||
* duration – ms before auto-dismiss (default 2000)
|
||
*/
|
||
export default function ShareToast({ message = 'Link copied!', visible = false, onHide, duration = 2000 }) {
|
||
const [show, setShow] = useState(false)
|
||
|
||
useEffect(() => {
|
||
if (visible) {
|
||
// Small delay so the enter transition plays
|
||
const enterTimer = requestAnimationFrame(() => setShow(true))
|
||
const hideTimer = setTimeout(() => {
|
||
setShow(false)
|
||
setTimeout(() => onHide?.(), 200) // let exit transition finish
|
||
}, duration)
|
||
|
||
return () => {
|
||
cancelAnimationFrame(enterTimer)
|
||
clearTimeout(hideTimer)
|
||
}
|
||
} else {
|
||
setShow(false)
|
||
}
|
||
}, [visible, duration, onHide])
|
||
|
||
if (!visible) return null
|
||
|
||
return createPortal(
|
||
<div
|
||
role="status"
|
||
aria-live="polite"
|
||
className={[
|
||
'fixed bottom-24 left-1/2 z-[10001] -translate-x-1/2 rounded-full border border-white/[0.10] bg-nova-800/90 px-5 py-2.5 text-sm font-medium text-white shadow-xl backdrop-blur-md transition-all duration-200',
|
||
show ? 'translate-y-0 opacity-100' : 'translate-y-3 opacity-0',
|
||
].join(' ')}
|
||
>
|
||
<span className="flex items-center gap-2">
|
||
{/* Check icon */}
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4 text-emerald-400">
|
||
<path fillRule="evenodd" d="M16.704 4.153a.75.75 0 0 1 .143 1.052l-8 10.5a.75.75 0 0 1-1.127.075l-4.5-4.5a.75.75 0 0 1 1.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 0 1 1.05-.143Z" clipRule="evenodd" />
|
||
</svg>
|
||
{message}
|
||
</span>
|
||
</div>,
|
||
document.body,
|
||
)
|
||
}
|