docs: Setup social sharing for blogposts (#8102)

This commit is contained in:
Adewale Abati
2026-03-25 15:37:08 +01:00
committed by GitHub
parent 5b4dd40593
commit 3a9805e255
7 changed files with 340 additions and 0 deletions
@@ -0,0 +1,130 @@
import React, { useState, useRef, useEffect, useMemo } from 'react';
import { Share2, Link, Check } from 'lucide-react';
import { IconTwitter } from '@site/src/components/icons/twitter';
import { IconLinkedIn } from '@site/src/components/icons/linkedin';
import { IconFacebook } from '@site/src/components/icons/facebook';
import { IconReddit } from '@site/src/components/icons/reddit';
import styles from './styles.module.css';
const TWITTER_VIA = 'goose_oss';
interface SocialShareProps {
url: string;
title: string;
}
const SocialShare: React.FC<SocialShareProps> = ({ url, title }) => {
const [open, setOpen] = useState(false);
const [copied, setCopied] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const shareOptions = useMemo(() => {
const encodedUrl = encodeURIComponent(url);
const encodedTitle = encodeURIComponent(title);
return [
{
name: 'Twitter / X',
icon: <IconTwitter />,
url: `https://twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}&via=${TWITTER_VIA}`,
},
{
name: 'LinkedIn',
icon: <IconLinkedIn />,
url: `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`,
},
{
name: 'Facebook',
icon: <IconFacebook />,
url: `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`,
},
{
name: 'Reddit',
icon: <IconReddit />,
url: `https://reddit.com/submit?url=${encodedUrl}&title=${encodedTitle}`,
},
];
}, [url, title]);
// Close menu on outside click or Escape
useEffect(() => {
if (!open) return;
const handleClickOutside = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setOpen(false);
}
};
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') setOpen(false);
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleEscape);
};
}, [open]);
const openShareWindow = (shareUrl: string) => {
window.open(shareUrl, '_blank', 'width=600,height=500,noopener,noreferrer');
setOpen(false);
};
const handleCopyLink = async () => {
try {
await navigator.clipboard.writeText(url);
setCopied(true);
setTimeout(() => {
setCopied(false);
setOpen(false);
}, 1500);
} catch (err) {
console.error('Failed to copy:', err);
}
};
return (
<div className={styles.wrapper} ref={menuRef}>
<button
onClick={() => setOpen((prev) => !prev)}
className={styles.shareButton}
aria-label="Share this post"
aria-expanded={open}
aria-haspopup="true"
title="Share this post"
>
<Share2 size={16} />
<span>Share</span>
</button>
{open && (
<div className={styles.dropdown} role="menu">
{shareOptions.map((option) => (
<button
key={option.name}
className={styles.dropdownItem}
onClick={() => openShareWindow(option.url)}
role="menuitem"
>
{option.icon}
<span>{option.name}</span>
</button>
))}
<div className={styles.divider} />
<button
className={styles.dropdownItem}
onClick={handleCopyLink}
role="menuitem"
>
{copied ? <Check size={16} /> : <Link size={16} />}
<span>{copied ? 'Copied!' : 'Copy link'}</span>
</button>
</div>
)}
</div>
);
};
export default SocialShare;
@@ -0,0 +1,129 @@
.wrapper {
position: relative;
display: inline-flex;
align-items: center;
}
/* ── Trigger button ───────────────────────────────── */
.shareButton {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.625rem;
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: 6px;
background: var(--ifm-background-color);
color: var(--ifm-font-color-base);
font-size: 0.8125rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
line-height: 1;
}
.shareButton:hover {
background: var(--ifm-color-primary);
color: #fff;
border-color: var(--ifm-color-primary);
}
.shareButton:focus-visible {
outline: 2px solid var(--ifm-color-primary);
outline-offset: 2px;
}
.shareButton svg {
flex-shrink: 0;
}
/* ── Dropdown menu ────────────────────────────────── */
.dropdown {
position: absolute;
top: calc(100% + 6px);
left: 0;
z-index: 100;
min-width: 180px;
padding: 0.375rem;
border: 1px solid var(--ifm-color-emphasis-200);
border-radius: 10px;
background: var(--ifm-background-color);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
animation: fadeIn 0.15s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ── Menu items ───────────────────────────────────── */
.dropdownItem {
display: flex;
align-items: center;
gap: 0.625rem;
width: 100%;
padding: 0.5rem 0.75rem;
border: none;
border-radius: 6px;
background: transparent;
color: var(--ifm-font-color-base);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: background 0.12s ease;
text-align: left;
}
.dropdownItem:hover {
background: var(--ifm-color-emphasis-100);
}
.dropdownItem:focus-visible {
outline: 2px solid var(--ifm-color-primary);
outline-offset: -2px;
}
.dropdownItem svg {
flex-shrink: 0;
opacity: 0.8;
}
.dropdownItem:hover svg {
opacity: 1;
}
/* ── Divider ──────────────────────────────────────── */
.divider {
height: 1px;
margin: 0.375rem 0.75rem;
background: var(--ifm-color-emphasis-200);
}
/* ── Dark mode ────────────────────────────────────── */
[data-theme='dark'] .shareButton {
background: var(--ifm-background-surface-color);
border-color: var(--ifm-color-emphasis-400);
}
[data-theme='dark'] .dropdown {
border-color: var(--ifm-color-emphasis-300);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}
[data-theme='dark'] .dropdownItem:hover {
background: var(--ifm-color-emphasis-200);
}
/* ── Print ────────────────────────────────────────── */
@media print {
.wrapper {
display: none;
}
}