import React, { useState, useEffect, useRef } from 'react'; import { createPortal } from 'react-dom'; import { useParams } from 'react-router-dom'; import { QRCodeSVG } from 'qrcode.react'; interface ShareBoardButtonProps { className?: string; } type PermissionType = 'view' | 'edit' | 'admin'; const PERMISSION_LABELS: Record = { view: { label: 'View', description: 'Can view but not edit', color: '#6b7280' }, edit: { label: 'Edit', description: 'Can view and edit', color: '#3b82f6' }, admin: { label: 'Admin', description: 'Full control', color: '#10b981' }, }; const ShareBoardButton: React.FC = ({ className = '' }) => { const { slug } = useParams<{ slug: string }>(); const [showDropdown, setShowDropdown] = useState(false); // Detect dark mode const [isDarkMode, setIsDarkMode] = useState( typeof document !== 'undefined' && document.documentElement.classList.contains('dark') ); useEffect(() => { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.attributeName === 'class') { setIsDarkMode(document.documentElement.classList.contains('dark')); } }); }); observer.observe(document.documentElement, { attributes: true }); return () => observer.disconnect(); }, []); const [copied, setCopied] = useState(false); const [permission, setPermission] = useState('edit'); const [nfcStatus, setNfcStatus] = useState<'idle' | 'writing' | 'success' | 'error' | 'unsupported'>('idle'); const [nfcMessage, setNfcMessage] = useState(''); const [showAdvanced, setShowAdvanced] = useState(false); const [inviteInput, setInviteInput] = useState(''); const [inviteStatus, setInviteStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle'); const dropdownRef = useRef(null); const dropdownMenuRef = useRef(null); const triggerRef = useRef(null); const [dropdownPosition, setDropdownPosition] = useState<{ top: number; right: number } | null>(null); const boardSlug = slug || 'mycofi33'; const boardUrl = `${window.location.origin}/board/${boardSlug}`; // Update dropdown position when it opens useEffect(() => { if (showDropdown && triggerRef.current) { const rect = triggerRef.current.getBoundingClientRect(); setDropdownPosition({ top: rect.bottom + 8, right: window.innerWidth - rect.right, }); } }, [showDropdown]); // Generate URL with permission parameter const getShareUrl = () => { const url = new URL(boardUrl); url.searchParams.set('access', permission); return url.toString(); }; // Check NFC support on mount useEffect(() => { if (!('NDEFReader' in window)) { setNfcStatus('unsupported'); } }, []); // Close dropdown when clicking outside or pressing ESC useEffect(() => { const handleClickOutside = (e: MouseEvent) => { const target = e.target as Node; // Check if click is inside trigger OR the portal dropdown menu const isInsideTrigger = dropdownRef.current && dropdownRef.current.contains(target); const isInsideMenu = dropdownMenuRef.current && dropdownMenuRef.current.contains(target); if (!isInsideTrigger && !isInsideMenu) { setShowDropdown(false); } }; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); setShowDropdown(false); } }; if (showDropdown) { document.addEventListener('mousedown', handleClickOutside); document.addEventListener('keydown', handleKeyDown, true); } return () => { document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('keydown', handleKeyDown, true); }; }, [showDropdown]); const handleCopyUrl = async () => { try { await navigator.clipboard.writeText(getShareUrl()); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (err) { console.error('Failed to copy URL:', err); } }; const handleInvite = async () => { if (!inviteInput.trim()) return; setInviteStatus('sending'); try { // TODO: Implement actual invite API call // For now, simulate sending invite await new Promise(resolve => setTimeout(resolve, 1000)); setInviteStatus('sent'); setInviteInput(''); setTimeout(() => setInviteStatus('idle'), 3000); } catch (err) { console.error('Failed to send invite:', err); setInviteStatus('error'); setTimeout(() => setInviteStatus('idle'), 3000); } }; const handleNfcWrite = async () => { if (!('NDEFReader' in window)) { setNfcStatus('unsupported'); setNfcMessage('NFC is not supported on this device'); return; } try { setNfcStatus('writing'); setNfcMessage('Hold your NFC tag near the device...'); const ndef = new (window as any).NDEFReader(); await ndef.write({ records: [ { recordType: "url", data: getShareUrl() } ] }); setNfcStatus('success'); setNfcMessage('Board URL written to NFC tag!'); setTimeout(() => { setNfcStatus('idle'); setNfcMessage(''); }, 3000); } catch (err: any) { console.error('NFC write error:', err); setNfcStatus('error'); if (err.name === 'NotAllowedError') { setNfcMessage('NFC permission denied. Please allow NFC access.'); } else if (err.name === 'NotSupportedError') { setNfcMessage('NFC is not supported on this device'); } else { setNfcMessage(`Failed to write NFC tag: ${err.message || 'Unknown error'}`); } } }; // Detect if we're in share-panel (compact) vs toolbar (full button) const isCompact = className.includes('share-panel-btn'); if (isCompact) { // Icon-only version for the top-right share panel with dropdown return (
{/* Dropdown - rendered via portal to break out of parent container */} {showDropdown && dropdownPosition && createPortal(
e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()} > {/* Compact Header */}
👥 Share Board
{/* Invite by username/email */}
setInviteInput(e.target.value)} onKeyDown={(e) => { e.stopPropagation(); if (e.key === 'Enter') handleInvite(); }} onPointerDown={(e) => e.stopPropagation()} onFocus={(e) => e.stopPropagation()} style={{ flex: 1, padding: '8px 12px', fontSize: '12px', fontFamily: 'inherit', border: '1px solid var(--color-panel-contrast)', borderRadius: '6px', background: 'var(--color-panel)', color: 'var(--color-text)', outline: 'none', }} />
{inviteStatus === 'error' && (

Failed to send invite. Please try again.

)}
{/* Divider with "or share link" */}
or share link
{/* Permission selector - pill style */}
{(['view', 'edit', 'admin'] as PermissionType[]).map((perm) => { const isActive = permission === perm; const { label, description } = PERMISSION_LABELS[perm]; return ( ); })}
{/* QR Code and URL - larger and side by side */}
{/* QR Code - larger */}
{/* URL and Copy - stacked */}
{getShareUrl()}
{/* Advanced options (collapsible) */}
{showAdvanced && (
{/* NFC Button */} {/* Audio Button (coming soon) */}
)} {nfcMessage && (

{nfcMessage}

)}
, document.body )}
); } // Full button version for other contexts (toolbar, etc.) return (
); }; export default ShareBoardButton;