Skip to content

Commit 88b23ee

Browse files
authored
Merge pull request #1647 from recodehive/copilot/add-social-sharing-buttons
Add end-of-post social share actions to blog posts
2 parents 58e615d + a6b8bf5 commit 88b23ee

3 files changed

Lines changed: 186 additions & 88 deletions

File tree

Lines changed: 73 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,100 @@
11
.blog-post-share-section {
2-
margin: 3rem 0;
3-
padding: 2rem;
4-
background: rgba(var(--ifm-color-primary-rgb), 0.05);
5-
border-radius: 16px;
6-
text-align: center;
7-
border: 1px solid rgba(var(--ifm-color-primary-rgb), 0.1);
2+
--share-btn-shadow: rgba(15, 23, 42, 0.06);
3+
--share-btn-shadow-hover: rgba(15, 23, 42, 0.1);
4+
margin-top: 2rem;
5+
padding-top: 1.5rem;
6+
border-top: 1px solid var(--ifm-color-emphasis-300);
87
}
98

109
.share-title {
11-
margin-bottom: 1.5rem;
12-
font-size: 1.25rem;
13-
font-weight: 700;
14-
color: var(--ifm-font-color-base);
10+
margin: 0 0 1rem;
11+
font-size: 0.95rem;
12+
font-weight: 600;
13+
color: var(--ifm-color-emphasis-700);
1514
}
1615

1716
.share-buttons-row {
1817
display: flex;
19-
flex-wrap: wrap;
18+
flex-wrap: nowrap;
2019
gap: 1rem;
21-
justify-content: center;
20+
align-items: center;
2221
}
2322

24-
.share-btn-large {
25-
display: flex;
23+
.share-btn-circle {
24+
display: inline-flex;
2625
align-items: center;
27-
gap: 0.75rem;
28-
padding: 0.8rem 1.5rem;
29-
border-radius: 50px;
30-
color: white !important;
31-
font-weight: 600;
26+
justify-content: center;
27+
width: 3.25rem;
28+
height: 3.25rem;
29+
border-radius: 999px;
30+
border: 1px solid var(--ifm-color-emphasis-300);
31+
background: var(--ifm-background-surface-color);
32+
color: var(--ifm-font-color-base) !important;
3233
text-decoration: none !important;
33-
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
34-
font-size: 0.95rem;
35-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
34+
transition:
35+
transform 0.2s ease,
36+
box-shadow 0.2s ease,
37+
border-color 0.2s ease,
38+
color 0.2s ease;
39+
box-shadow: 0 12px 30px var(--share-btn-shadow);
40+
cursor: pointer;
41+
}
42+
43+
.share-btn-circle svg {
44+
width: 1.15rem;
45+
height: 1.15rem;
3646
}
3747

38-
.share-btn-large:hover {
39-
transform: translateY(-3px);
40-
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
41-
filter: brightness(1.1);
48+
.share-btn-circle:hover {
49+
transform: translateY(-2px);
50+
box-shadow: 0 18px 36px var(--share-btn-shadow-hover);
51+
text-decoration: none;
4252
}
4353

44-
.share-btn-large.twitter {
45-
background-color: #000000;
54+
.share-btn-circle:focus-visible {
55+
outline: 2px solid var(--ifm-color-primary);
56+
outline-offset: 2px;
4657
}
4758

48-
.share-btn-large.linkedin {
49-
background-color: #0077b5;
59+
.share-btn-circle.x:hover {
60+
border-color: #111827;
61+
color: #111827 !important;
5062
}
5163

52-
.share-btn-large.facebook {
53-
background-color: #1877f2;
64+
.share-btn-circle.linkedin:hover {
65+
border-color: #0a66c2;
66+
color: #0a66c2 !important;
67+
}
68+
69+
.share-btn-circle.email:hover {
70+
border-color: #ea580c;
71+
color: #ea580c !important;
72+
}
73+
74+
.share-btn-circle.copy:hover,
75+
.share-btn-circle.copy.copied {
76+
border-color: var(--ifm-color-primary);
77+
color: var(--ifm-color-primary) !important;
78+
}
79+
80+
.share-btn-circle.copy.copy-error {
81+
border-color: var(--ifm-color-danger);
82+
color: var(--ifm-color-danger) !important;
83+
}
84+
85+
@supports (color: color-mix(in srgb, white 50%, black)) {
86+
.share-btn-circle {
87+
border-color: color-mix(
88+
in srgb,
89+
var(--ifm-color-primary) 18%,
90+
var(--ifm-color-emphasis-300)
91+
);
92+
}
5493
}
5594

5695
@media (max-width: 576px) {
5796
.share-buttons-row {
58-
flex-direction: column;
59-
align-items: stretch;
60-
}
61-
62-
.share-btn-large {
63-
justify-content: center;
97+
gap: 0.75rem;
98+
flex-wrap: wrap;
6499
}
65100
}
Lines changed: 109 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,117 @@
1-
import React from 'react';
2-
import { useBlogPost } from '@docusaurus/plugin-content-blog/client';
3-
import { FaTwitter, FaLinkedin, FaFacebook } from 'react-icons/fa';
4-
import './SocialShare.css';
5-
6-
const SocialShare = () => {
7-
// Safe hook call
8-
let blogPost;
9-
try {
10-
blogPost = useBlogPost();
11-
} catch (e) {
1+
import React, { useEffect, useState } from "react";
2+
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
3+
import { FaEnvelope, FaLink, FaLinkedinIn } from "react-icons/fa";
4+
import { FaXTwitter } from "react-icons/fa6";
5+
6+
import "./SocialShare.css";
7+
8+
const COPY_RESET_DELAY_MS = 2000;
9+
const COPY_DEFAULT_LABEL = "Copy link";
10+
const COPY_SUCCESS_LABEL = "Link copied";
11+
const COPY_ERROR_LABEL = "Unable to copy link";
12+
13+
type SocialShareProps = {
14+
permalink?: string;
15+
title?: string;
16+
};
17+
18+
export default function SocialShare({
19+
permalink,
20+
title,
21+
}: SocialShareProps): JSX.Element | null {
22+
const { siteConfig } = useDocusaurusContext();
23+
const [copyState, setCopyState] = useState<"idle" | "copied" | "error">("idle");
24+
25+
useEffect(() => {
26+
if (copyState === "idle") {
27+
return undefined;
28+
}
29+
30+
const timeout = window.setTimeout(() => setCopyState("idle"), COPY_RESET_DELAY_MS);
31+
32+
return () => window.clearTimeout(timeout);
33+
}, [copyState]);
34+
35+
if (!permalink || !title) {
1236
return null;
1337
}
14-
15-
if (!blogPost) return null;
16-
17-
const { metadata } = blogPost;
18-
const { permalink, title } = metadata;
19-
const blogUrl = `https://www.recodehive.com${permalink}`;
20-
const shareText = encodeURIComponent(`Check out this article: ${title}`);
38+
39+
const blogUrl = new URL(permalink, siteConfig.url).toString();
40+
const shareText = `Check out this article: ${title}`;
41+
const encodedBlogUrl = encodeURIComponent(blogUrl);
42+
const encodedShareText = encodeURIComponent(shareText);
43+
44+
const shareLinks = [
45+
{
46+
className: "x",
47+
href: `https://twitter.com/intent/tweet?text=${encodedShareText}&url=${encodedBlogUrl}`,
48+
icon: <FaXTwitter aria-hidden="true" />,
49+
label: "Share on X",
50+
},
51+
{
52+
className: "linkedin",
53+
href: `https://www.linkedin.com/sharing/share-offsite/?url=${encodedBlogUrl}`,
54+
icon: <FaLinkedinIn aria-hidden="true" />,
55+
label: "Share on LinkedIn",
56+
},
57+
{
58+
className: "email",
59+
href: `mailto:?subject=${encodedShareText}&body=${encodeURIComponent(`${shareText}\n\n${blogUrl}`)}`,
60+
icon: <FaEnvelope aria-hidden="true" />,
61+
label: "Share by email",
62+
},
63+
];
64+
65+
const handleCopyLink = async () => {
66+
if (typeof navigator === "undefined" || !navigator.clipboard) {
67+
setCopyState("error");
68+
return;
69+
}
70+
71+
try {
72+
await navigator.clipboard.writeText(blogUrl);
73+
setCopyState("copied");
74+
} catch {
75+
setCopyState("error");
76+
}
77+
};
78+
79+
const copyLabel =
80+
copyState === "copied"
81+
? COPY_SUCCESS_LABEL
82+
: copyState === "error"
83+
? COPY_ERROR_LABEL
84+
: COPY_DEFAULT_LABEL;
2185

2286
return (
23-
<div className="blog-post-share-section">
24-
<h3 className="share-title">Enjoyed the article? Share it!</h3>
87+
<section className="blog-post-share-section" aria-label="Share this post">
88+
<p className="share-title">Share this post</p>
2589
<div className="share-buttons-row">
26-
<a
27-
href={`https://twitter.com/intent/tweet?text=${shareText}&url=${encodeURIComponent(blogUrl)}`}
28-
target="_blank"
29-
rel="noopener noreferrer"
30-
className="share-btn-large twitter"
31-
title="Share on X (Twitter)"
90+
{shareLinks.map((link) => (
91+
<a
92+
key={link.label}
93+
href={link.href}
94+
target="_blank"
95+
rel="noopener noreferrer"
96+
className={`share-btn-circle ${link.className}`}
97+
title={link.label}
98+
aria-label={link.label}
99+
>
100+
{link.icon}
101+
</a>
102+
))}
103+
<button
104+
type="button"
105+
className={`share-btn-circle copy${copyState === "copied" ? " copied" : ""}${copyState === "error" ? " copy-error" : ""}`}
106+
onClick={() => {
107+
void handleCopyLink();
108+
}}
109+
title={copyLabel}
110+
aria-label={copyLabel}
32111
>
33-
<FaTwitter /> <span>Share on X</span>
34-
</a>
35-
<a
36-
href={`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(blogUrl)}`}
37-
target="_blank"
38-
rel="noopener noreferrer"
39-
className="share-btn-large linkedin"
40-
title="Share on LinkedIn"
41-
>
42-
<FaLinkedin /> <span>Share on LinkedIn</span>
43-
</a>
44-
<a
45-
href={`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(blogUrl)}`}
46-
target="_blank"
47-
rel="noopener noreferrer"
48-
className="share-btn-large facebook"
49-
title="Share on Facebook"
50-
>
51-
<FaFacebook /> <span>Share on Facebook</span>
52-
</a>
112+
<FaLink aria-hidden="true" />
113+
</button>
53114
</div>
54-
</div>
115+
</section>
55116
);
56-
};
57-
58-
export default SocialShare;
117+
}

src/theme/BlogPostItem/Footer/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useBlogPost } from "@docusaurus/plugin-content-blog/client";
55
import BlogPostItemFooterOriginal from "@theme-original/BlogPostItem/Footer";
66
import type BlogPostItemFooterType from "@theme/BlogPostItem/Footer";
77
import type { WrapperProps } from "@docusaurus/types";
8+
import SocialShare from "../../../components/SocialShare";
89
import { getAuthorProfile } from "../../../utils/authors";
910

1011
import styles from "./styles.module.css";
@@ -94,6 +95,9 @@ export default function BlogPostItemFooterWrapper(props: Props): JSX.Element {
9495
return (
9596
<>
9697
<BlogPostItemFooterOriginal {...props} />
98+
{isBlogPostPage && (
99+
<SocialShare permalink={metadata.permalink} title={metadata.title} />
100+
)}
97101
{showAuthorCard && (
98102
<section className={styles.authorCard} aria-label="Post author details">
99103
<div className={styles.authorBody}>

0 commit comments

Comments
 (0)