Skip to main content
Glama
northernvariables

FedMCP - Federal Parliamentary Information

ShareButton.tsx11.3 kB
/** * ShareButton Component * * Universal share button with social media sharing, clipboard, email, and print. * Uses Web Share API on mobile when available, falls back to dropdown menu via portal. */ 'use client'; import { useState, useRef, useEffect } from 'react'; import { createPortal } from 'react-dom'; import { useTranslations } from 'next-intl'; import { Share2, Link2, Mail, Printer, Check, Facebook, Linkedin, Twitter, } from 'lucide-react'; import { useShare, type ShareData } from '@/hooks/useShare'; import { cn } from '@canadagpt/design-system'; export interface ShareButtonProps { url: string; title: string; description?: string; className?: string; variant?: 'icon' | 'button'; size?: 'sm' | 'md' | 'lg'; } export function ShareButton({ url, title, description, className, variant = 'icon', size = 'md', }: ShareButtonProps) { const t = useTranslations('share'); const [isOpen, setIsOpen] = useState(false); const [mounted, setMounted] = useState(false); const buttonRef = useRef<HTMLButtonElement>(null); const dropdownRef = useRef<HTMLDivElement>(null); const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 }); const shareData: ShareData = { url, title, description }; const { isSupported, isCopied, shareNative, copyToClipboard, shareEmail, sharePrint, shareTwitter, shareFacebook, shareLinkedIn, shareReddit, shareEHSocial, shareThreads, } = useShare(shareData); // Handle mounting useEffect(() => { setMounted(true); }, []); // Calculate dropdown position when opened useEffect(() => { if (isOpen && buttonRef.current) { const rect = buttonRef.current.getBoundingClientRect(); const dropdownWidth = 224; // w-56 = 14rem = 224px setDropdownPosition({ top: rect.bottom + 8, // 8px gap below button left: rect.right - dropdownWidth, // Right-align with button }); } }, [isOpen]); // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( dropdownRef.current && !dropdownRef.current.contains(event.target as Node) && buttonRef.current && !buttonRef.current.contains(event.target as Node) ) { setIsOpen(false); } }; if (isOpen) { document.addEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [isOpen]); const handleShareClick = async (e: React.MouseEvent) => { // Stop propagation to prevent parent Link from being triggered e.preventDefault(); e.stopPropagation(); // Calculate position BEFORE opening to prevent flicker if (!isOpen && buttonRef.current) { const rect = buttonRef.current.getBoundingClientRect(); const dropdownWidth = 224; // w-56 = 14rem = 224px setDropdownPosition({ top: rect.bottom + 8, // 8px gap below button left: rect.right - dropdownWidth, // Right-align with button }); } // Always show dropdown menu for consistent experience across devices setIsOpen(!isOpen); }; const handleCopyClick = async () => { try { await copyToClipboard(); // Keep dropdown open to show "Copied!" feedback } catch (err) { console.error('Failed to copy:', err); } }; const handleOptionClick = (action: () => void) => { action(); setIsOpen(false); }; const sizeClasses = { sm: 'p-2', md: 'p-2.5', lg: 'p-3', }; const iconSizes = { sm: 18, md: 20, lg: 24, }; const dropdownContent = isOpen && mounted && ( <div ref={dropdownRef} style={{ position: 'fixed', top: `${dropdownPosition.top}px`, left: `${dropdownPosition.left}px`, zIndex: 9999, }} className=" w-56 bg-bg-secondary border-2 border-border rounded-lg shadow-2xl py-2 " > {/* Primary action: Copy link */} <button onClick={handleCopyClick} className=" w-full px-4 py-2.5 text-left hover:bg-bg-hover transition-colors flex items-center gap-3 border-b border-border-subtle mb-1 pb-3 " > {isCopied ? ( <> <Check size={18} className="text-green-500" /> <span className="text-sm font-medium text-green-500"> {t('linkCopied')} </span> </> ) : ( <> <Link2 size={18} className="text-text-secondary" /> <span className="text-sm font-medium text-text-primary"> {t('copyLink')} </span> </> )} </button> {/* Email */} <button onClick={() => handleOptionClick(shareEmail)} className=" w-full px-4 py-2 text-left hover:bg-bg-hover transition-colors flex items-center gap-3 " > <Mail size={18} className="text-text-secondary" /> <span className="text-sm text-text-primary">{t('email')}</span> </button> {/* Print */} <button onClick={() => handleOptionClick(sharePrint)} className=" w-full px-4 py-2 text-left hover:bg-bg-hover transition-colors flex items-center gap-3 border-b border-border-subtle mb-1 pb-3 " > <Printer size={18} className="text-text-secondary" /> <span className="text-sm text-text-primary">{t('print')}</span> </button> {/* Social media heading */} <div className="px-4 py-1.5"> <p className="text-xs font-semibold text-text-tertiary uppercase tracking-wide"> {t('shareVia')} </p> </div> {/* X (Twitter) */} <button onClick={() => handleOptionClick(shareTwitter)} className=" w-full px-4 py-2 text-left hover:bg-bg-hover transition-colors flex items-center gap-3 " > <Twitter size={18} className="text-text-secondary" /> <span className="text-sm text-text-primary">{t('twitter')}</span> </button> {/* Facebook */} <button onClick={() => handleOptionClick(shareFacebook)} className=" w-full px-4 py-2 text-left hover:bg-bg-hover transition-colors flex items-center gap-3 " > <Facebook size={18} className="text-text-secondary" /> <span className="text-sm text-text-primary">{t('facebook')}</span> </button> {/* LinkedIn */} <button onClick={() => handleOptionClick(shareLinkedIn)} className=" w-full px-4 py-2 text-left hover:bg-bg-hover transition-colors flex items-center gap-3 " > <Linkedin size={18} className="text-text-secondary" /> <span className="text-sm text-text-primary">{t('linkedin')}</span> </button> {/* Reddit */} <button onClick={() => handleOptionClick(shareReddit)} className=" w-full px-4 py-2 text-left hover:bg-bg-hover transition-colors flex items-center gap-3 " > <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" className="text-text-secondary" > <path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z" /> </svg> <span className="text-sm text-text-primary">{t('reddit')}</span> </button> {/* EH! Social */} <button onClick={() => handleOptionClick(shareEHSocial)} className=" w-full px-4 py-2 text-left hover:bg-bg-hover transition-colors flex items-center gap-3 " > <div className="w-[18px] h-[18px] flex items-center justify-center text-text-secondary font-bold text-xs"> EH! </div> <span className="text-sm text-text-primary">{t('ehSocial')}</span> </button> {/* Threads */} <button onClick={() => handleOptionClick(shareThreads)} className=" w-full px-4 py-2 text-left hover:bg-bg-hover transition-colors flex items-center gap-3 " > <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-text-secondary" > <path d="M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" /> <path d="M12 8v8" /> <path d="M8 12h8" /> </svg> <span className="text-sm text-text-primary">{t('threads')}</span> </button> </div> ); return ( <> {/* Share button */} <button ref={buttonRef} onClick={handleShareClick} className={cn( 'rounded-lg border-2 border-border shadow-md transition-colors', 'bg-bg-secondary/80 backdrop-blur-sm', 'text-text-primary hover:text-accent-red hover:border-accent-red hover:shadow-lg', sizeClasses[size], className )} aria-label={t('button')} title={t('button')} > <Share2 size={iconSizes[size]} /> </button> {/* Dropdown menu rendered via portal */} {mounted && dropdownContent && createPortal(dropdownContent, document.body)} </> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/northernvariables/FedMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server