From 3a9805e255fbb6254e788b6dabdba47ef51fddd9 Mon Sep 17 00:00:00 2001 From: Adewale Abati Date: Wed, 25 Mar 2026 15:37:08 +0100 Subject: [PATCH] docs: Setup social sharing for blogposts (#8102) --- .../src/components/SocialShare/index.tsx | 130 ++++++++++++++++++ .../components/SocialShare/styles.module.css | 129 +++++++++++++++++ .../src/components/icons/facebook.tsx | 12 ++ .../src/components/icons/linkedin.tsx | 12 ++ documentation/src/components/icons/reddit.tsx | 12 ++ .../src/components/icons/twitter.tsx | 12 ++ .../theme/BlogPostItem/Header/Info/index.tsx | 33 +++++ 7 files changed, 340 insertions(+) create mode 100644 documentation/src/components/SocialShare/index.tsx create mode 100644 documentation/src/components/SocialShare/styles.module.css create mode 100644 documentation/src/components/icons/facebook.tsx create mode 100644 documentation/src/components/icons/linkedin.tsx create mode 100644 documentation/src/components/icons/reddit.tsx create mode 100644 documentation/src/components/icons/twitter.tsx create mode 100644 documentation/src/theme/BlogPostItem/Header/Info/index.tsx diff --git a/documentation/src/components/SocialShare/index.tsx b/documentation/src/components/SocialShare/index.tsx new file mode 100644 index 0000000000..8c08c28faf --- /dev/null +++ b/documentation/src/components/SocialShare/index.tsx @@ -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 = ({ url, title }) => { + const [open, setOpen] = useState(false); + const [copied, setCopied] = useState(false); + const menuRef = useRef(null); + + const shareOptions = useMemo(() => { + const encodedUrl = encodeURIComponent(url); + const encodedTitle = encodeURIComponent(title); + + return [ + { + name: 'Twitter / X', + icon: , + url: `https://twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}&via=${TWITTER_VIA}`, + }, + { + name: 'LinkedIn', + icon: , + url: `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`, + }, + { + name: 'Facebook', + icon: , + url: `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`, + }, + { + name: 'Reddit', + icon: , + 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 ( +
+ + + {open && ( +
+ {shareOptions.map((option) => ( + + ))} +
+ +
+ )} +
+ ); +}; + +export default SocialShare; diff --git a/documentation/src/components/SocialShare/styles.module.css b/documentation/src/components/SocialShare/styles.module.css new file mode 100644 index 0000000000..f005da0e55 --- /dev/null +++ b/documentation/src/components/SocialShare/styles.module.css @@ -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; + } +} diff --git a/documentation/src/components/icons/facebook.tsx b/documentation/src/components/icons/facebook.tsx new file mode 100644 index 0000000000..fde2d495a2 --- /dev/null +++ b/documentation/src/components/icons/facebook.tsx @@ -0,0 +1,12 @@ +export const IconFacebook = ({ className = "" }) => ( + +); diff --git a/documentation/src/components/icons/linkedin.tsx b/documentation/src/components/icons/linkedin.tsx new file mode 100644 index 0000000000..4e66b214b9 --- /dev/null +++ b/documentation/src/components/icons/linkedin.tsx @@ -0,0 +1,12 @@ +export const IconLinkedIn = ({ className = "" }) => ( + +); diff --git a/documentation/src/components/icons/reddit.tsx b/documentation/src/components/icons/reddit.tsx new file mode 100644 index 0000000000..2d83a17f1c --- /dev/null +++ b/documentation/src/components/icons/reddit.tsx @@ -0,0 +1,12 @@ +export const IconReddit = ({ className = "" }) => ( + +); diff --git a/documentation/src/components/icons/twitter.tsx b/documentation/src/components/icons/twitter.tsx new file mode 100644 index 0000000000..cac2685294 --- /dev/null +++ b/documentation/src/components/icons/twitter.tsx @@ -0,0 +1,12 @@ +export const IconTwitter = ({ className = "" }) => ( + +); diff --git a/documentation/src/theme/BlogPostItem/Header/Info/index.tsx b/documentation/src/theme/BlogPostItem/Header/Info/index.tsx new file mode 100644 index 0000000000..6329c6f9f3 --- /dev/null +++ b/documentation/src/theme/BlogPostItem/Header/Info/index.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import Info from '@theme-original/BlogPostItem/Header/Info'; +import type InfoType from '@theme/BlogPostItem/Header/Info'; +import type { WrapperProps } from '@docusaurus/types'; +import { useBlogPost } from '@docusaurus/plugin-content-blog/client'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import SocialShare from '@site/src/components/SocialShare'; + +type Props = WrapperProps; + +function buildPostUrl(siteUrl: string, permalink: string): string { + const base = siteUrl.endsWith('/') ? siteUrl.slice(0, -1) : siteUrl; + return `${base}${permalink}`; +} + +export default function InfoWrapper(props: Props): JSX.Element { + const { metadata, isBlogPostPage } = useBlogPost(); + const { siteConfig } = useDocusaurusContext(); + + const postUrl = buildPostUrl(siteConfig.url, metadata.permalink); + + return ( +
+ + {isBlogPostPage && ( + <> + · + + + )} +
+ ); +}