diff --git a/.github/copilot/integrate-analytics.prompt.md b/.github/copilot/integrate-analytics.prompt.md index 60cd595..cc8f4c6 100644 --- a/.github/copilot/integrate-analytics.prompt.md +++ b/.github/copilot/integrate-analytics.prompt.md @@ -41,14 +41,14 @@ forwards to whatever tool they choose. ## 2 — Core events catalogue -| `eventName` | `interactionType` | Fires when | -| ---------------------------- | ----------------- | ----------------------------------------------- | -| `social_share_popup_open` | `popup_open` | Share modal/popup opens | -| `social_share_popup_close` | `popup_close` | Modal closes (button, overlay, or Esc key) | -| `social_share_click` | `share` | User clicks a platform button (share intent) | -| `social_share_success` | `share` | Platform share window opened successfully | -| `social_share_copy` | `copy` | User copies the link to clipboard | -| `social_share_error` | `error` | Share or copy action failed | +| `eventName` | `interactionType` | Fires when | +| -------------------------- | ----------------- | -------------------------------------------- | +| `social_share_popup_open` | `popup_open` | Share modal/popup opens | +| `social_share_popup_close` | `popup_close` | Modal closes (button, overlay, or Esc key) | +| `social_share_click` | `share` | User clicks a platform button (share intent) | +| `social_share_success` | `share` | Platform share window opened successfully | +| `social_share_copy` | `copy` | User copies the link to clipboard | +| `social_share_error` | `error` | Share or copy action failed | --- @@ -59,7 +59,7 @@ forwards to whatever tool they choose. ```js // Fires on the container element and bubbles through the DOM (composed:true // means it also crosses shadow-DOM boundaries). -document.addEventListener("social-share", (e) => { +document.addEventListener('social-share', (e) => { const payload = e.detail; // Forward to your analytics tool here }); @@ -72,7 +72,7 @@ and Mixpanel both need the same event. ```js new SocialShareButton({ - container: "#share-button", + container: '#share-button', onAnalytics: (payload) => { // Forward to your analytics tool here }, @@ -96,11 +96,8 @@ Load the adapters file **in addition to** the main library script: const { GoogleAnalyticsAdapter, MixpanelAdapter } = window.SocialShareAnalytics; new SocialShareButton({ - container: "#share-button", - analyticsPlugins: [ - new GoogleAnalyticsAdapter(), - new MixpanelAdapter(), - ], + container: '#share-button', + analyticsPlugins: [new GoogleAnalyticsAdapter(), new MixpanelAdapter()], }); ``` @@ -125,7 +122,7 @@ Prerequisite: GA4 `gtag.js` snippet loaded by the host. const { GoogleAnalyticsAdapter } = window.SocialShareAnalytics; new SocialShareButton({ - container: "#share-button", + container: '#share-button', analyticsPlugins: [new GoogleAnalyticsAdapter()], }); @@ -135,7 +132,7 @@ new SocialShareButton({ Custom event category (optional): ```js -new GoogleAnalyticsAdapter({ eventCategory: "engagement" }) +new GoogleAnalyticsAdapter({ eventCategory: 'engagement' }); ``` ### Mixpanel @@ -145,7 +142,7 @@ Prerequisite: `mixpanel-browser` snippet or SDK loaded. ```js const { MixpanelAdapter } = window.SocialShareAnalytics; new SocialShareButton({ - container: "#share-button", + container: '#share-button', analyticsPlugins: [new MixpanelAdapter()], }); // Calls: mixpanel.track(eventName, { platform, url, ... }) @@ -156,7 +153,7 @@ new SocialShareButton({ ```js const { SegmentAdapter } = window.SocialShareAnalytics; new SocialShareButton({ - container: "#share-button", + container: '#share-button', analyticsPlugins: [new SegmentAdapter()], }); // Calls: analytics.track(eventName, { platform, url, ... }) @@ -169,7 +166,7 @@ Prerequisite: Plausible `script.js` loaded with custom events enabled. ```js const { PlausibleAdapter } = window.SocialShareAnalytics; new SocialShareButton({ - container: "#share-button", + container: '#share-button', analyticsPlugins: [new PlausibleAdapter()], }); // Calls: plausible(eventName, { props: { platform, url, ... } }) @@ -180,7 +177,7 @@ new SocialShareButton({ ```js const { PostHogAdapter } = window.SocialShareAnalytics; new SocialShareButton({ - container: "#share-button", + container: '#share-button', analyticsPlugins: [new PostHogAdapter()], }); // Calls: posthog.capture(eventName, { platform, url, ... }) @@ -195,8 +192,8 @@ const { CustomAdapter } = window.SocialShareAnalytics; new SocialShareButton({ analyticsPlugins: [ new CustomAdapter((payload) => { - fetch("/api/analytics", { - method: "POST", + fetch('/api/analytics', { + method: 'POST', body: JSON.stringify(payload), }); }), @@ -231,17 +228,17 @@ GA4, Mixpanel, and a custom endpoint at the same time: ```js const { GoogleAnalyticsAdapter, MixpanelAdapter, CustomAdapter } = window.SocialShareAnalytics; -document.addEventListener("social-share", (e) => { - console.log("Raw event:", e.detail); // Debugging / logging +document.addEventListener('social-share', (e) => { + console.log('Raw event:', e.detail); // Debugging / logging }); new SocialShareButton({ - container: "#share-button", - componentId: "homepage-hero", + container: '#share-button', + componentId: 'homepage-hero', analyticsPlugins: [ new GoogleAnalyticsAdapter(), new MixpanelAdapter(), - new CustomAdapter((p) => fetch("/log", { method: "POST", body: JSON.stringify(p) })), + new CustomAdapter((p) => fetch('/log', { method: 'POST', body: JSON.stringify(p) })), ], }); ``` @@ -255,7 +252,7 @@ development. Remove or set to `false` in production. ```js new SocialShareButton({ - container: "#share-button", + container: '#share-button', debug: true, // → [SocialShareButton Analytics] { version: '1.0', source: 'social-share-button', ... } }); @@ -271,7 +268,7 @@ instrumentation must be explicitly consented to before activation. ```js new SocialShareButton({ - container: "#share-button", + container: '#share-button', analytics: false, }); ``` diff --git a/.github/copilot/integrate-social-share-button.prompt.md b/.github/copilot/integrate-social-share-button.prompt.md index 3b7a24b..ffd571d 100644 --- a/.github/copilot/integrate-social-share-button.prompt.md +++ b/.github/copilot/integrate-social-share-button.prompt.md @@ -25,11 +25,11 @@ You are helping a developer integrate the **SocialShareButton** library The README defines **3 installation methods**. Ask (or infer) which the developer wants: -| Method | When to use | -|--------|-------------| -| **Method 1 — CDN (Recommended)** | Most projects. No build step needed. Load via ` ``` @@ -99,8 +102,8 @@ No framework. Just add the CDN tags directly: **Step 2:** Open an **existing** component that renders on every page — typically `src/components/Header.jsx`, `src/layouts/MainLayout.jsx`, or your root `App.jsx`. Add the snippet below to that component so the share button is consistently available across your app. ```jsx -import { useEffect, useRef } from "react"; -import { useLocation } from "react-router-dom"; // omit if not using React Router +import { useEffect, useRef } from 'react'; +import { useLocation } from 'react-router-dom'; // omit if not using React Router // ⬇️ Replace 'Header' with the name of the component where you want the // share button to appear — e.g. Navbar, MainLayout, App, etc. @@ -113,7 +116,7 @@ function Header() { if (initRef.current || !window.SocialShareButton) return; shareButtonRef.current = new window.SocialShareButton({ - container: "#share-button", + container: '#share-button', }); initRef.current = true; @@ -150,13 +153,9 @@ function Header() { **Step 1:** Add CDN to `app/layout.tsx`: ```tsx -import Script from "next/script"; +import Script from 'next/script'; -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( @@ -180,10 +179,10 @@ export default function RootLayout({ **Step 2:** Because `SocialShareButton` manipulates the DOM, it must run inside a **Client Component** (note the `"use client"` directive at the top). Add the snippet below to an existing component such as `app/components/Header.tsx` or `app/components/Navbar.tsx` — any component already included in your layout. ```tsx -"use client"; +'use client'; -import { useEffect, useRef } from "react"; -import { usePathname } from "next/navigation"; +import { useEffect, useRef } from 'react'; +import { usePathname } from 'next/navigation'; // ⬇️ Replace 'Header' with the name of the component where you want the // share button to appear — e.g. Navbar, MainLayout, App, etc. @@ -195,11 +194,10 @@ export default function Header() { useEffect(() => { const initButton = () => { - if (initRef.current || !window.SocialShareButton || !containerRef.current) - return; + if (initRef.current || !window.SocialShareButton || !containerRef.current) return; shareButtonRef.current = new window.SocialShareButton({ - container: "#share-button", + container: '#share-button', }); initRef.current = true; }; @@ -262,7 +260,7 @@ declare global { **Step 1:** Add CDN to `pages/_document.tsx`: ```tsx -import { Html, Head, Main, NextScript } from "next/document"; +import { Html, Head, Main, NextScript } from 'next/document'; export default function Document() { return ( @@ -286,8 +284,8 @@ export default function Document() { **Step 2:** Open an existing component that is rendered on every page — typically `components/Header.tsx`, `components/Navbar.tsx`, or `components/Layout.tsx`. Since `_document.tsx` loads the script globally, the button is ready to initialize in any of these components. ```tsx -import { useEffect, useRef } from "react"; -import { useRouter } from "next/router"; +import { useEffect, useRef } from 'react'; +import { useRouter } from 'next/router'; // ⬇️ Replace 'Header' with the name of the component where you want the // share button to appear — e.g. Navbar, MainLayout, App, etc. @@ -299,11 +297,10 @@ export default function Header() { useEffect(() => { const initButton = () => { - if (initRef.current || !window.SocialShareButton || !containerRef.current) - return; + if (initRef.current || !window.SocialShareButton || !containerRef.current) return; shareButtonRef.current = new window.SocialShareButton({ - container: "#share-button", + container: '#share-button', }); initRef.current = true; }; @@ -384,7 +381,7 @@ declare global { // Add
to your component's template/HTML first, // then initialize once the DOM is ready (e.g., in mounted(), ngAfterViewInit(), or useEffect()): new window.SocialShareButton({ - container: "#share-button", + container: '#share-button', }); ``` @@ -395,10 +392,10 @@ new window.SocialShareButton({ Use when the project has a bundler (Webpack, Vite, etc.) and the developer prefers `import` syntax. Works in any framework. ```javascript -import SocialShareButton from "social-share-button-aossie"; -import "social-share-button-aossie/src/social-share-button.css"; +import SocialShareButton from 'social-share-button-aossie'; +import 'social-share-button-aossie/src/social-share-button.css'; -new SocialShareButton({ container: "#share-button" }); +new SocialShareButton({ container: '#share-button' }); ``` > No CDN tags needed — the npm package includes both JS and CSS. @@ -412,10 +409,10 @@ Only use this when the developer **explicitly** wants a reusable JSX component. Tell them to copy `src/social-share-button-react.jsx` from the library into their project — **do not create a new file from scratch**. ```jsx -import { SocialShareButton } from "./components/SocialShareButton"; +import { SocialShareButton } from './components/SocialShareButton'; function App() { - return ; + return ; } ``` @@ -423,30 +420,30 @@ function App() { ## All constructor options -| Option | Type | Default | Description | -| ------------------ | -------------- | ---------------------- | -------------------------------------------------- | -| `container` | string/Element | — | **Required.** CSS selector or DOM element | -| `url` | string | `window.location.href` | URL to share | -| `title` | string | `document.title` | Share title/headline | -| `description` | string | `''` | Additional description text | -| `hashtags` | array | `[]` | e.g. `['js', 'webdev']` | -| `via` | string | `''` | Twitter handle (without @) | +| Option | Type | Default | Description | +| ------------------ | -------------- | ---------------------- | ---------------------------------------------------------- | +| `container` | string/Element | — | **Required.** CSS selector or DOM element | +| `url` | string | `window.location.href` | URL to share | +| `title` | string | `document.title` | Share title/headline | +| `description` | string | `''` | Additional description text | +| `hashtags` | array | `[]` | e.g. `['js', 'webdev']` | +| `via` | string | `''` | Twitter handle (without @) | | `platforms` | array | All platforms | `whatsapp facebook twitter linkedin telegram reddit email` | -| `buttonText` | string | `'Share'` | Button label text | -| `buttonStyle` | string | `'default'` | `default` `primary` `compact` `icon-only` | -| `buttonColor` | string | `''` | Custom button background color | -| `buttonHoverColor` | string | `''` | Custom button hover color | -| `customClass` | string | `''` | Additional CSS class for button | -| `theme` | string | `'dark'` | `dark` or `light` | -| `modalPosition` | string | `'center'` | Modal position on screen | -| `showButton` | boolean | `true` | Show/hide the share button | -| `onShare` | function | `null` | `(platform, url) => void` | -| `onCopy` | function | `null` | `(url) => void` | -| `analytics` | boolean | `true` | Set `false` to disable all event emission | -| `onAnalytics` | function | `null` | `(payload) => void` — direct analytics hook | -| `analyticsPlugins` | array | `[]` | Adapter instances from `social-share-analytics.js` | -| `componentId` | string | `null` | Label this instance for analytics tracking | -| `debug` | boolean | `false` | Log analytics events to console | +| `buttonText` | string | `'Share'` | Button label text | +| `buttonStyle` | string | `'default'` | `default` `primary` `compact` `icon-only` | +| `buttonColor` | string | `''` | Custom button background color | +| `buttonHoverColor` | string | `''` | Custom button hover color | +| `customClass` | string | `''` | Additional CSS class for button | +| `theme` | string | `'dark'` | `dark` or `light` | +| `modalPosition` | string | `'center'` | Modal position on screen | +| `showButton` | boolean | `true` | Show/hide the share button | +| `onShare` | function | `null` | `(platform, url) => void` | +| `onCopy` | function | `null` | `(url) => void` | +| `analytics` | boolean | `true` | Set `false` to disable all event emission | +| `onAnalytics` | function | `null` | `(payload) => void` — direct analytics hook | +| `analyticsPlugins` | array | `[]` | Adapter instances from `social-share-analytics.js` | +| `componentId` | string | `null` | Label this instance for analytics tracking | +| `debug` | boolean | `false` | Log analytics events to console | --- @@ -469,7 +466,7 @@ const shareButton = useRef(null); useEffect(() => { shareButton.current = new window.SocialShareButton({ - container: "#share-button", + container: '#share-button', }); }, []); @@ -487,25 +484,25 @@ useEffect(() => { ## Troubleshooting -| Symptom | Cause | Fix | -|---------|-------|-----| -| Multiple buttons appearing | Component re-renders creating duplicate instances | Use `useRef` + `initRef` guard (shown in all examples above) | -| Button not appearing | Script loads after component renders | Add `if (window.SocialShareButton)` null check | -| Modal not opening | CSS not loaded or ID mismatch | Verify CSS CDN in ``; match `container: '#share-button'` with `
` | -| `TypeError: SocialShareButton is not a constructor` | CDN script not loaded yet | Use interval polling (see Next.js examples above) | -| URL not updating on navigation | Component initialized once, doesn't track routes | Use `updateOptions()` on route change | +| Symptom | Cause | Fix | +| --------------------------------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| Multiple buttons appearing | Component re-renders creating duplicate instances | Use `useRef` + `initRef` guard (shown in all examples above) | +| Button not appearing | Script loads after component renders | Add `if (window.SocialShareButton)` null check | +| Modal not opening | CSS not loaded or ID mismatch | Verify CSS CDN in ``; match `container: '#share-button'` with `
` | +| `TypeError: SocialShareButton is not a constructor` | CDN script not loaded yet | Use interval polling (see Next.js examples above) | +| URL not updating on navigation | Component initialized once, doesn't track routes | Use `updateOptions()` on route change | --- ## Common mistakes to prevent -| ❌ Wrong | ✅ Correct | -|---------|-----------| -| Creating `ShareButton.jsx` / `ShareButton.tsx` | Add directly to existing `Header.jsx`, `Navbar.tsx`, etc. | -| Calling `new SocialShareButton()` inside JSX `return` | Call only inside `useEffect` / lifecycle hook | -| Not calling `destroy()` on unmount | Always clean up — prevents duplicate modals on re-mount | -| Mismatched container ID | `container: '#share-button'` must exactly match `
` | -| Script loads after component renders in Next.js | Use `strategy="beforeInteractive"` **or** poll with `setInterval` | +| ❌ Wrong | ✅ Correct | +| ----------------------------------------------------- | ------------------------------------------------------------------------- | +| Creating `ShareButton.jsx` / `ShareButton.tsx` | Add directly to existing `Header.jsx`, `Navbar.tsx`, etc. | +| Calling `new SocialShareButton()` inside JSX `return` | Call only inside `useEffect` / lifecycle hook | +| Not calling `destroy()` on unmount | Always clean up — prevents duplicate modals on re-mount | +| Mismatched container ID | `container: '#share-button'` must exactly match `
` | +| Script loads after component renders in Next.js | Use `strategy="beforeInteractive"` **or** poll with `setInterval` | --- @@ -516,4 +513,3 @@ useEffect(() => { - Always modify **existing** files — never suggest creating new component files. - When modifying an existing file, mark additions with `// ADD THIS`. - Do not add abstractions, wrappers, or extra files beyond what the README shows. - diff --git a/README.md b/README.md index bf8c605..6baa77d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@
- > ⚠️ **IMPORTANT** > > All project discussions happens on **[Discord](https://discord.com/channels/1022871757289422898/1479012884209078365)**. @@ -73,7 +72,7 @@ Lightweight social sharing component for web applications. Zero dependencies, fr - 🌐 Multiple platforms: WhatsApp, Facebook, X, LinkedIn, Telegram, Reddit, Email - 🎯 Zero dependencies - pure vanilla JavaScript -- ⚛️ Framework support: React, Next.js, Vue, Angular, or plain HTML +- ⚛️ Framework support: React, Next.js, Vue, Angular, Remix, or plain HTML - 🔄 Auto-detects current URL and page title - 📱 Fully responsive and mobile-ready - 🎨 Customizable themes (dark/light) @@ -86,7 +85,10 @@ Lightweight social sharing component for web applications. Zero dependencies, fr ### Via CDN (Recommended) ```html - + ``` @@ -101,11 +103,11 @@ Lightweight social sharing component for web applications. Zero dependencies, fr No matter which framework you use, integration always follows the same 3 steps: -| Step | What to do | Where | -|------|-----------|-------| -| **1️⃣ Load Library** | Add CSS + JS (CDN links) | Global layout file — `index.html` / `layout.tsx` / `_document.tsx` | -| **2️⃣ Add Container** | Place `
` | The UI component where you want the button to appear | -| **3️⃣ Initialize** | Call `new SocialShareButton({ container: "#share-button" })` | Inside that component, after the DOM is ready (e.g. `useEffect`, `mounted`, `ngAfterViewInit`) | +| Step | What to do | Where | +| -------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | +| **1️⃣ Load Library** | Add CSS + JS (CDN links) | Global layout file — `index.html` / `layout.tsx` / `_document.tsx` | +| **2️⃣ Add Container** | Place `
` | The UI component where you want the button to appear | +| **3️⃣ Initialize** | Call `new SocialShareButton({ container: "#share-button" })` | Inside that component, after the DOM is ready (e.g. `useEffect`, `mounted`, `ngAfterViewInit`) | > 💡 Pick your framework below for the full copy-paste snippet: @@ -132,8 +134,8 @@ No matter which framework you use, integration always follows the same 3 steps: Open an **existing** component that renders on every page — typically `src/components/Header.jsx`, `src/layouts/MainLayout.jsx`, or your root `App.jsx`. Add the snippet below to that component so the share button is consistently available across your app. ```jsx -import { useEffect, useRef } from "react"; -import { useLocation } from "react-router-dom"; // omit if not using React Router +import { useEffect, useRef } from 'react'; +import { useLocation } from 'react-router-dom'; // omit if not using React Router // ⬇️ Replace 'Header' with the name of the component where you want the // share button to appear — e.g. Navbar, MainLayout, App, etc. @@ -146,7 +148,7 @@ function Header() { if (initRef.current || !window.SocialShareButton) return; shareButtonRef.current = new window.SocialShareButton({ - container: "#share-button", + container: '#share-button', }); initRef.current = true; @@ -184,13 +186,9 @@ function Header() { ### Step 1: Add CDN to `app/layout.tsx` ```tsx -import Script from "next/script"; +import Script from 'next/script'; -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( @@ -216,10 +214,10 @@ export default function RootLayout({ Because `SocialShareButton` manipulates the DOM, it must run inside a **Client Component** (note the `"use client"` directive at the top). Add the snippet below to an existing component such as `app/components/Header.tsx` or `app/components/Navbar.tsx` — any component already included in your layout. ```tsx -"use client"; +'use client'; -import { useEffect, useRef } from "react"; -import { usePathname } from "next/navigation"; +import { useEffect, useRef } from 'react'; +import { usePathname } from 'next/navigation'; // ⬇️ Replace 'Header' with the name of the component where you want the // share button to appear — e.g. Navbar, MainLayout, App, etc. @@ -231,11 +229,10 @@ export default function Header() { useEffect(() => { const initButton = () => { - if (initRef.current || !window.SocialShareButton || !containerRef.current) - return; + if (initRef.current || !window.SocialShareButton || !containerRef.current) return; shareButtonRef.current = new window.SocialShareButton({ - container: "#share-button", + container: '#share-button', }); initRef.current = true; }; @@ -299,7 +296,7 @@ declare global { ### Step 1: Add CDN to `pages/_document.tsx` ```tsx -import { Html, Head, Main, NextScript } from "next/document"; +import { Html, Head, Main, NextScript } from 'next/document'; export default function Document() { return ( @@ -325,8 +322,8 @@ export default function Document() { Open an existing component that is rendered on every page — typically `components/Header.tsx`, `components/Navbar.tsx`, or `components/Layout.tsx`. Since `_document.tsx` loads the script globally, the button is ready to initialize in any of these components. ```tsx -import { useEffect, useRef } from "react"; -import { useRouter } from "next/router"; +import { useEffect, useRef } from 'react'; +import { useRouter } from 'next/router'; // ⬇️ Replace 'Header' with the name of the component where you want the // share button to appear — e.g. Navbar, MainLayout, App, etc. @@ -338,11 +335,10 @@ export default function Header() { useEffect(() => { const initButton = () => { - if (initRef.current || !window.SocialShareButton || !containerRef.current) - return; + if (initRef.current || !window.SocialShareButton || !containerRef.current) return; shareButtonRef.current = new window.SocialShareButton({ - container: "#share-button", + container: '#share-button', }); initRef.current = true; }; @@ -426,12 +422,59 @@ Open your root or layout component (e.g., `App.vue`, `app.component.html`, or `A // Add
to your component's template/HTML first, // then initialize once the DOM is ready (e.g., in mounted(), ngAfterViewInit(), or useEffect()): new window.SocialShareButton({ - container: "#share-button", + container: '#share-button', }); ``` +
+🎸 Remix + +### Step 1: Load CSS via `links` export and JS via CDN script in `app/root.tsx` + +```tsx +import type { LinksFunction } from '@remix-run/node'; + +export const links: LinksFunction = () => [ + { + rel: 'stylesheet', + href: 'https://cdn.jsdelivr.net/gh/AOSSIE-Org/SocialShareButton@v1.0.3/src/social-share-button.css', + }, +]; +``` + +Add the CDN ` +``` + +### Step 2: Copy `src/social-share-button-remix.jsx` into your project and use it in a route + +```tsx +// app/routes/_index.tsx +import SocialShareButton from '~/components/social-share-button-remix'; + +export default function Index() { + return ( + + ); +} +``` + +> **Note:** Remix is SSR-first. The component wraps all browser-API calls inside +> `useEffect` with an `typeof window !== 'undefined'` guard so it renders safely +> on the server and only initialises `SocialShareButton` in the browser. + +
+ --- ## Configuration @@ -440,13 +483,13 @@ new window.SocialShareButton({ ```jsx new SocialShareButton({ - container: "#share-button", // Required: CSS selector or DOM element - url: "https://example.com", // Optional: defaults to window.location.href - title: "Custom Title", // Optional: defaults to document.title - buttonText: "Share", // Optional: button label text - buttonStyle: "primary", // default | primary | compact | icon-only - theme: "dark", // dark | light - platforms: ["twitter", "linkedin"], // Optional: defaults to all platforms + container: '#share-button', // Required: CSS selector or DOM element + url: 'https://example.com', // Optional: defaults to window.location.href + title: 'Custom Title', // Optional: defaults to document.title + buttonText: 'Share', // Optional: button label text + buttonStyle: 'primary', // default | primary | compact | icon-only + theme: 'dark', // dark | light + platforms: ['twitter', 'linkedin'], // Optional: defaults to all platforms }); ``` @@ -481,12 +524,12 @@ Control the text that appears when users share to social platforms: ```jsx new SocialShareButton({ - container: "#share-button", - url: "https://myproject.com", - title: "Check out my awesome project!", // Main title/headline - description: "An amazing tool for developers", // Additional description - hashtags: ["javascript", "webdev", "opensource"], // Hashtags included in posts - via: "MyProjectHandle", // Your Twitter handle + container: '#share-button', + url: 'https://myproject.com', + title: 'Check out my awesome project!', // Main title/headline + description: 'An amazing tool for developers', // Additional description + hashtags: ['javascript', 'webdev', 'opensource'], // Hashtags included in posts + via: 'MyProjectHandle', // Your Twitter handle }); ``` @@ -506,8 +549,8 @@ new SocialShareButton({ ```jsx new SocialShareButton({ - container: "#share-button", - buttonStyle: "primary", // or 'default', 'compact', 'icon-only' + container: '#share-button', + buttonStyle: 'primary', // or 'default', 'compact', 'icon-only' }); ``` @@ -517,9 +560,9 @@ Pass `buttonColor` and `buttonHoverColor` to match your project's color scheme: ```jsx new SocialShareButton({ - container: "#share-button", - buttonColor: "#ff6b6b", // Button background color - buttonHoverColor: "#ff5252", // Hover state color + container: '#share-button', + buttonColor: '#ff6b6b', // Button background color + buttonHoverColor: '#ff5252', // Hover state color }); ``` @@ -529,9 +572,9 @@ For more complex styling, use a custom CSS class: ```jsx new SocialShareButton({ - container: "#share-button", - buttonStyle: "primary", - customClass: "my-custom-button", + container: '#share-button', + buttonStyle: 'primary', + customClass: 'my-custom-button', }); ``` @@ -555,23 +598,23 @@ Then in your CSS file: ```jsx // Material Design Red new SocialShareButton({ - container: "#share-button", - buttonColor: "#f44336", - buttonHoverColor: "#da190b", + container: '#share-button', + buttonColor: '#f44336', + buttonHoverColor: '#da190b', }); // Tailwind Blue new SocialShareButton({ - container: "#share-button", - buttonColor: "#3b82f6", - buttonHoverColor: "#2563eb", + container: '#share-button', + buttonColor: '#3b82f6', + buttonHoverColor: '#2563eb', }); // Custom Brand Color new SocialShareButton({ - container: "#share-button", - buttonColor: "#your-brand-color", - buttonHoverColor: "#your-brand-color-dark", + container: '#share-button', + buttonColor: '#your-brand-color', + buttonHoverColor: '#your-brand-color-dark', }); ``` @@ -588,12 +631,12 @@ new SocialShareButton({ ```jsx new SocialShareButton({ - container: "#share-button", + container: '#share-button', onShare: (platform, url) => { console.log(`Shared on ${platform}: ${url}`); }, onCopy: (url) => { - console.log("Link copied:", url); + console.log('Link copied:', url); }, }); ``` @@ -605,11 +648,11 @@ new SocialShareButton({ ### Using npm Package ```javascript -import SocialShareButton from "social-share-button-aossie"; -import "social-share-button-aossie/src/social-share-button.css"; +import SocialShareButton from 'social-share-button-aossie'; +import 'social-share-button-aossie/src/social-share-button.css'; new SocialShareButton({ - container: "#share-button", + container: '#share-button', }); ``` @@ -618,10 +661,10 @@ new SocialShareButton({ If you want a reusable React component, copy `src/social-share-button-react.jsx` to your project: ```jsx -import { SocialShareButton } from "./components/SocialShareButton"; +import { SocialShareButton } from './components/SocialShareButton'; function App() { - return ; + return ; } ``` @@ -640,7 +683,7 @@ const shareButton = useRef(null); useEffect(() => { shareButton.current = new window.SocialShareButton({ - container: "#share-button", + container: '#share-button', }); }, []); @@ -676,7 +719,7 @@ useEffect(() => { ```jsx if (window.SocialShareButton) { - new window.SocialShareButton({ container: "#share-button" }); + new window.SocialShareButton({ container: '#share-button' }); } ``` @@ -729,14 +772,14 @@ if (window.SocialShareButton) { ```jsx // Professional networks only new SocialShareButton({ - container: "#share-button", - platforms: ["linkedin", "twitter", "email"], + container: '#share-button', + platforms: ['linkedin', 'twitter', 'email'], }); // Messaging apps only new SocialShareButton({ - container: "#share-button", - platforms: ["whatsapp", "telegram"], + container: '#share-button', + platforms: ['whatsapp', 'telegram'], }); ``` @@ -744,9 +787,9 @@ new SocialShareButton({ ```jsx new SocialShareButton({ - container: "#share-button", - buttonStyle: "icon-only", - theme: "light", + container: '#share-button', + buttonStyle: 'icon-only', + theme: 'light', }); ``` diff --git a/docs/client-guide.md b/docs/client-guide.md index 6b49350..da7d57d 100644 --- a/docs/client-guide.md +++ b/docs/client-guide.md @@ -8,12 +8,12 @@ Your audience is already sharing. Give them a button worth clicking. -| What you get | What you skip | -|---|---| -| One-line install | Backend servers | +| What you get | What you skip | +| ---------------------- | --------------------------- | +| One-line install | Backend servers | | Works on any framework | Tracking or data collection | -| Fully customizable | Lock-in or licensing fees | -| Lightweight & fast | Build step (CDN option) | +| Fully customizable | Lock-in or licensing fees | +| Lightweight & fast | Build step (CDN option) | --- diff --git a/eslint.config.js b/eslint.config.js index 701e191..9b85c82 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,13 +1,13 @@ -import js from "@eslint/js"; -import globals from "globals"; +import js from '@eslint/js'; +import globals from 'globals'; export default [ js.configs.recommended, { - files: ["src/**/*.{js,jsx}"], + files: ['src/**/*.{js,jsx}'], languageOptions: { - ecmaVersion: "latest", - sourceType: "module", + ecmaVersion: 'latest', + sourceType: 'module', parserOptions: { ecmaFeatures: { jsx: true, @@ -20,15 +20,15 @@ export default [ }, }, rules: { - "no-console": "error", - "no-unused-vars": ["warn", { "caughtErrorsIgnorePattern": "^_" }], - "semi": ["error", "always"], + 'no-console': 'error', + 'no-unused-vars': ['warn', { caughtErrorsIgnorePattern: '^_' }], + semi: ['error', 'always'], }, }, { - files: ["eslint.config.js", "**/*.config.js"], + files: ['eslint.config.js', '**/*.config.js'], languageOptions: { - sourceType: "module", + sourceType: 'module', globals: { ...globals.node, }, diff --git a/index.html b/index.html index 90db0da..ae4d812 100644 --- a/index.html +++ b/index.html @@ -397,7 +397,7 @@

🚀 Quick Start

Get started in seconds with minimal setup

- + <link rel="stylesheet" href="social-share-button.css"> @@ -419,10 +419,10 @@

⚛️ React Integration

First-class React support with hooks

- + -import SocialShareButton from 'social-share-button'; +import SocialShareButton from 'social-share-button'; function App() { return ( @@ -438,6 +438,59 @@

⚛️ React Integration

+ +
+

🎸 Remix Integration

+

+ Remix is an SSR-first React framework — all browser APIs must live inside + useEffect. + Copy src/social-share-button-remix.jsx + into your project and follow the steps below. +

+ +

Step 1 — Load the CSS & JS in app/root.tsx

+
+ + + +// app/root.tsx +import type { LinksFunction } from "@remix-run/node"; + +export const links: LinksFunction = () => [ + { + rel: "stylesheet", + href: "https://cdn.jsdelivr.net/npm/social-share-button/dist/social-share-button.css", + }, +]; + +// In the <head> of your root component, add: +// <Scripts /> loads the Remix client bundle. +// Place the CDN <script> just before </body>: +// <script src="https://cdn.jsdelivr.net/npm/social-share-button/dist/social-share-button.js"></script> +
+ +

Step 2 — Use the component in a route

+
+ + + +// app/routes/_index.tsx +import SocialShareButton from "~/components/social-share-button-remix"; + +export default function Index() { + return ( + <SocialShareButton + url="https://your-website.com" + title="Check this out!" + description="An amazing website" + theme="dark" + buttonText="Share" + /> + ); +} +
+
+

Ready to Get Started?

diff --git a/src/social-share-analytics.js b/src/social-share-analytics.js index 0caf0d7..4b5fd3a 100644 --- a/src/social-share-analytics.js +++ b/src/social-share-analytics.js @@ -123,14 +123,14 @@ class GoogleAnalyticsAdapter extends SocialShareAnalyticsPlugin { */ constructor(config = {}) { super(); - this.eventCategory = config.eventCategory || "social_share"; + this.eventCategory = config.eventCategory || 'social_share'; } track(payload) { - if (typeof window === "undefined" || typeof window.gtag !== "function") { + if (typeof window === 'undefined' || typeof window.gtag !== 'function') { return; } - window.gtag("event", payload.eventName, { + window.gtag('event', payload.eventName, { event_category: this.eventCategory, event_label: payload.platform, share_platform: payload.platform, @@ -151,9 +151,9 @@ class GoogleAnalyticsAdapter extends SocialShareAnalyticsPlugin { class MixpanelAdapter extends SocialShareAnalyticsPlugin { track(payload) { if ( - typeof window === "undefined" || - typeof window.mixpanel === "undefined" || - typeof window.mixpanel.track !== "function" + typeof window === 'undefined' || + typeof window.mixpanel === 'undefined' || + typeof window.mixpanel.track !== 'function' ) { return; } @@ -177,9 +177,9 @@ class MixpanelAdapter extends SocialShareAnalyticsPlugin { class SegmentAdapter extends SocialShareAnalyticsPlugin { track(payload) { if ( - typeof window === "undefined" || - typeof window.analytics === "undefined" || - typeof window.analytics.track !== "function" + typeof window === 'undefined' || + typeof window.analytics === 'undefined' || + typeof window.analytics.track !== 'function' ) { return; } @@ -201,7 +201,7 @@ class SegmentAdapter extends SocialShareAnalyticsPlugin { // ----------------------------------------------------------------------------- class PlausibleAdapter extends SocialShareAnalyticsPlugin { track(payload) { - if (typeof window === "undefined" || typeof window.plausible !== "function") { + if (typeof window === 'undefined' || typeof window.plausible !== 'function') { return; } window.plausible(payload.eventName, { @@ -223,9 +223,9 @@ class PlausibleAdapter extends SocialShareAnalyticsPlugin { class PostHogAdapter extends SocialShareAnalyticsPlugin { track(payload) { if ( - typeof window === "undefined" || - typeof window.posthog === "undefined" || - typeof window.posthog.capture !== "function" + typeof window === 'undefined' || + typeof window.posthog === 'undefined' || + typeof window.posthog.capture !== 'function' ) { return; } @@ -256,8 +256,8 @@ class CustomAdapter extends SocialShareAnalyticsPlugin { */ constructor(onTrack) { super(); - if (typeof onTrack !== "function") { - throw new TypeError("CustomAdapter expects a function argument."); + if (typeof onTrack !== 'function') { + throw new TypeError('CustomAdapter expects a function argument.'); } this._onTrack = onTrack; } @@ -281,10 +281,10 @@ const adapters = { CustomAdapter, }; -if (typeof module !== "undefined" && module.exports) { +if (typeof module !== 'undefined' && module.exports) { module.exports = adapters; } -if (typeof window !== "undefined") { +if (typeof window !== 'undefined') { window.SocialShareAnalytics = adapters; } diff --git a/src/social-share-button-react.jsx b/src/social-share-button-react.jsx index 43bf022..b60a5fe 100644 --- a/src/social-share-button-react.jsx +++ b/src/social-share-button-react.jsx @@ -1,46 +1,37 @@ -import { useEffect, useRef } from "react"; +import { useEffect, useRef } from 'react'; export const SocialShareButton = ({ url, title, - description = "", + description = '', hashtags = [], - via = "", - platforms = [ - "whatsapp", - "facebook", - "twitter", - "linkedin", - "telegram", - "reddit", - ], - theme = "dark", - buttonText = "Share", - customClass = "", + via = '', + platforms = ['whatsapp', 'facebook', 'twitter', 'linkedin', 'telegram', 'reddit'], + theme = 'dark', + buttonText = 'Share', + customClass = '', onShare = null, onCopy = null, - buttonStyle = "default", - modalPosition = "center", + buttonStyle = 'default', + modalPosition = 'center', // Analytics props — the library itself never collects data. // Provide any combination to connect your own analytics tools. - analytics = true, // set to false to disable all event emission - onAnalytics = null, // (payload) => void — direct callback hook - analyticsPlugins = [], // array of adapter instances (see social-share-analytics.js) - componentId = null, // optional string identifier for this instance - debug = false, // log events to console during development + analytics = true, // set to false to disable all event emission + onAnalytics = null, // (payload) => void — direct callback hook + analyticsPlugins = [], // array of adapter instances (see social-share-analytics.js) + componentId = null, // optional string identifier for this instance + debug = false, // log events to console during development }) => { const containerRef = useRef(null); const shareButtonRef = useRef(null); // Auto-detect current URL and title if not provided - const currentUrl = - url || (typeof window !== "undefined" ? window.location.href : ""); - const currentTitle = - title || (typeof document !== "undefined" ? document.title : ""); + const currentUrl = url || (typeof window !== 'undefined' ? window.location.href : ''); + const currentTitle = title || (typeof document !== 'undefined' ? document.title : ''); useEffect(() => { if (containerRef.current && !shareButtonRef.current) { - if (typeof window !== "undefined" && window.SocialShareButton) { + if (typeof window !== 'undefined' && window.SocialShareButton) { shareButtonRef.current = new window.SocialShareButton({ container: containerRef.current, url: currentUrl, diff --git a/src/social-share-button-remix.jsx b/src/social-share-button-remix.jsx new file mode 100644 index 0000000..7dde4c8 --- /dev/null +++ b/src/social-share-button-remix.jsx @@ -0,0 +1,138 @@ +import { useEffect, useRef } from 'react'; + +/** + * SocialShareButton — Remix-compatible React component. + * + * Remix is SSR-first and has no `'use client'` directive, so all browser-API + * access must be gated inside a `useEffect` hook. This component: + * 1. Initialises the vanilla `window.SocialShareButton` in the first effect + * (runs only in the browser, never on the server). + * 2. Calls `destroy()` on unmount to prevent memory leaks. + * 3. Calls `updateOptions()` whenever any prop changes so the button always + * reflects the current route URL / page title without a full re-mount. + * + * @example + * // app/routes/_index.tsx + * import SocialShareButton from '~/components/social-share-button-remix'; + * + * export default function Index() { + * return ( + * + * ); + * } + */ +export default function SocialShareButton({ + url = '', + title = '', + description = '', + hashtags = [], + via = '', + platforms = ['whatsapp', 'facebook', 'twitter', 'linkedin', 'telegram', 'reddit'], + theme = 'dark', + buttonText = 'Share', + customClass = '', + onShare = null, + onCopy = null, + buttonStyle = 'default', + modalPosition = 'center', + analytics = false, + onAnalytics = null, + analyticsPlugins = [], + componentId = null, + debug = false, +}) { + const containerRef = useRef(null); + const shareButtonRef = useRef(null); + + const currentUrl = url || (typeof window !== 'undefined' ? window.location.href : ''); + const currentTitle = title || (typeof document !== 'undefined' ? document.title : ''); + + // ── Mount: initialise once in the browser ────────────────────────────── + useEffect(() => { + // SSR guard — Remix always server-renders; browser APIs only available here. + if (typeof window === 'undefined' || !window.SocialShareButton) return; + + shareButtonRef.current = new window.SocialShareButton({ + container: containerRef.current, + url: currentUrl, + title: currentTitle, + description, + hashtags, + via, + platforms, + theme, + buttonText, + customClass, + onShare, + onCopy, + buttonStyle, + modalPosition, + analytics, + onAnalytics, + analyticsPlugins, + componentId, + debug, + }); + + // Cleanup on unmount + return () => { + if (shareButtonRef.current) { + shareButtonRef.current.destroy(); + shareButtonRef.current = null; + } + }; + }, []); + + // ── Prop updates: keep the button in sync without re-mounting ────────── + useEffect(() => { + if (!shareButtonRef.current) return; + + shareButtonRef.current.updateOptions({ + url: currentUrl, + title: currentTitle, + description, + hashtags, + via, + platforms, + theme, + buttonText, + customClass, + onShare, + onCopy, + buttonStyle, + modalPosition, + analytics, + onAnalytics, + analyticsPlugins, + componentId, + debug, + }); + }, [ + currentUrl, + currentTitle, + description, + hashtags, + via, + platforms, + theme, + buttonText, + customClass, + onShare, + onCopy, + buttonStyle, + modalPosition, + analytics, + onAnalytics, + analyticsPlugins, + componentId, + debug, + ]); + + return
; +} diff --git a/src/social-share-button.css b/src/social-share-button.css index 0c90b36..3bde1aa 100644 --- a/src/social-share-button.css +++ b/src/social-share-button.css @@ -19,8 +19,7 @@ cursor: pointer; transition: all 0.3s ease; font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, - Cantarell, sans-serif; + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; outline: none; } @@ -262,8 +261,7 @@ text-align: center; max-width: 80px; font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, - Cantarell, sans-serif; + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; } .social-share-modal-overlay.light .social-share-platform-btn span { @@ -299,8 +297,7 @@ font-size: 14px; outline: none; font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, - Cantarell, sans-serif; + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; } .social-share-modal-overlay.light .social-share-link-input input { @@ -324,8 +321,7 @@ transition: all 0.2s ease; min-width: 80px; font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, - Cantarell, sans-serif; + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; outline: none; } diff --git a/src/social-share-button.js b/src/social-share-button.js index 5f05397..686766f 100644 --- a/src/social-share-button.js +++ b/src/social-share-button.js @@ -5,39 +5,35 @@ */ /** Analytics event schema version. Increment when the payload shape changes. */ -const ANALYTICS_SCHEMA_VERSION = "1.0"; +const ANALYTICS_SCHEMA_VERSION = '1.0'; class SocialShareButton { constructor(options = {}) { this.options = { - url: - options.url || - (typeof window !== "undefined" ? window.location.href : ""), - title: - options.title || - (typeof document !== "undefined" ? document.title : ""), - description: options.description || "", + url: options.url || (typeof window !== 'undefined' ? window.location.href : ''), + title: options.title || (typeof document !== 'undefined' ? document.title : ''), + description: options.description || '', hashtags: options.hashtags || [], - via: options.via || "", + via: options.via || '', platforms: options.platforms || [ - "whatsapp", - "facebook", - "twitter", - "linkedin", - "telegram", - "reddit", + 'whatsapp', + 'facebook', + 'twitter', + 'linkedin', + 'telegram', + 'reddit', ], - theme: options.theme || "dark", - buttonText: options.buttonText || "Share", - customClass: options.customClass || "", - buttonColor: options.buttonColor || "", - buttonHoverColor: options.buttonHoverColor || "", + theme: options.theme || 'dark', + buttonText: options.buttonText || 'Share', + customClass: options.customClass || '', + buttonColor: options.buttonColor || '', + buttonHoverColor: options.buttonHoverColor || '', onShare: options.onShare || null, onCopy: options.onCopy || null, container: options.container || null, showButton: options.showButton !== false, - buttonStyle: options.buttonStyle || "default", - modalPosition: options.modalPosition || "center", + buttonStyle: options.buttonStyle || 'default', + modalPosition: options.modalPosition || 'center', // Analytics — the library emits events but never collects or sends data itself. // Website owners wire up their own analytics tools via these options. analytics: options.analytics !== false, // set to false to disable all event emission @@ -55,14 +51,13 @@ class SocialShareButton { this.handleKeydown = null; this.listeners = []; // Central registry for all event listeners - this.openTimeout = null; // Track setTimeout for openModal animation + this.openTimeout = null; // Track setTimeout for openModal animation this.closeTimeout = null; // Track setTimeout for closeModal animation this.feedbackTimeout = null; // Track setTimeout for copy feedback reset this.ownsBodyLock = false; // Track if this instance owns the body overflow lock this.eventsAttached = false; // Guard against multiple attachEvents() calls this.isDestroyed = false; // Track if instance has been destroyed (prevents async callbacks) - if (this.options.container) { this.init(); } @@ -78,9 +73,9 @@ class SocialShareButton { } createButton() { - const button = document.createElement("button"); + const button = document.createElement('button'); button.className = `social-share-btn ${this.options.buttonStyle} ${this.options.customClass}`; - button.setAttribute("aria-label", "Share"); + button.setAttribute('aria-label', 'Share'); button.innerHTML = `