docs: Setup social sharing for blogposts (#8102)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user