├── .eslintignore ├── config ├── setup-test-env.js └── jest-preprocess.js ├── .github ├── ISSUE_TEMPLATE │ ├── question-template.md │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── build.yml │ └── stale.yml └── PULL_REQUEST_TEMPLATE.md ├── src ├── index.js ├── gatsby-node.js ├── utils.js └── components │ ├── CommentEmbed.jsx │ ├── CommentCount.jsx │ └── Disqus.jsx ├── .babelrc ├── jest.config.js ├── .npmignore ├── types └── index.d.ts ├── LICENSE ├── __tests__ ├── CommentEmbed.js ├── Disqus.js └── CommentCount.js ├── docs ├── CODE_OF_CONDUCT.md ├── CHANGELOG.md └── CONTRIBUTING.md ├── package.json ├── .gitignore ├── README.md └── .eslintrc /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.d.ts 2 | lib/* 3 | **/*.md 4 | -------------------------------------------------------------------------------- /config/setup-test-env.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question template 3 | about: Ask a question about the project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /config/jest-preprocess.js: -------------------------------------------------------------------------------- 1 | const babelOptions = { 2 | presets: ['babel-preset-gatsby-package'], 3 | plugins: ['@babel/plugin-transform-spread'], 4 | }; 5 | 6 | module.exports = require('babel-jest').createTransformer(babelOptions); 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Disqus from './components/Disqus'; 2 | import CommentCount from './components/CommentCount'; 3 | import CommentEmbed from './components/CommentEmbed'; 4 | 5 | 6 | export { CommentCount, CommentEmbed, Disqus }; 7 | export default Disqus; 8 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["babel-preset-gatsby-package", { "browser": true }] 4 | ], 5 | "plugins": [ 6 | "@babel/plugin-transform-spread", 7 | ["@babel/plugin-proposal-private-methods", { "loose": false }] 8 | ], 9 | "ignore": ["src/__test__/"] 10 | } 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFilesAfterEnv: ['/config/setup-test-env.js'], 3 | }; 4 | module.exports = { 5 | transform: { 6 | '^.+\\.jsx?$': `/config/jest-preprocess.js`, 7 | }, 8 | setupFilesAfterEnv: ['/config/setup-test-env.js'], 9 | testPathIgnorePatterns: [`node_modules`, `\\.cache`, `.*/public`], 10 | transformIgnorePatterns: [`node_modules/(?!(gatsby)/)`], 11 | globals: { 12 | __PATH_PREFIX__: ``, 13 | GATSBY_DISQUS_SHORTNAME: 'testing', 14 | }, 15 | testURL: `http://localhost`, 16 | }; 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/gatsby-node.js: -------------------------------------------------------------------------------- 1 | let didRunAlready = false; 2 | let shortname = ''; 3 | 4 | exports.onPreInit = function(_ref, pluginOptions) { 5 | // Gatsby adds a pluginOptions that's not needed for this plugin 6 | delete pluginOptions.plugins; 7 | shortname = pluginOptions.shortname; 8 | 9 | if (didRunAlready) { 10 | throw new Error('You can only have single instance of gatsby-plugin-disqus in your gatsby-config.js'); 11 | } 12 | didRunAlready = true; 13 | }; 14 | 15 | exports.onCreateWebpackConfig = ({ plugins, actions }) => { 16 | const setWebpackConfig = actions.setWebpackConfig; 17 | setWebpackConfig({ 18 | plugins: [plugins.define({ 19 | GATSBY_DISQUS_SHORTNAME: JSON.stringify(shortname), 20 | })], 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # GitHub Templates 26 | .github 27 | 28 | # Docs 29 | docs 30 | 31 | # Tests 32 | __tests__ 33 | 34 | # Dependency directory 35 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 36 | node_modules 37 | config 38 | yarn.lock 39 | .release* 40 | src 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us address an issue 4 | title: '' 5 | labels: unconfirmed 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Specifications:** 27 | - OS: 28 | - Package version: 29 | - Gatsby version: 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type DisqusConfig = { 4 | url?: string; 5 | identifier?: string; 6 | title?: string; 7 | }; 8 | 9 | type DisqusEmbedConfig = DisqusConfig & { 10 | language?: string; 11 | remoteAuthS3?: string; 12 | apiKey?: string; 13 | }; 14 | 15 | declare module 'gatsby-plugin-disqus' { 16 | export interface DisqusEmbedProps { 17 | config?: DisqusEmbedConfig 18 | className?: string; 19 | } 20 | export class Disqus extends React.Component {} 21 | 22 | export interface CommentCountProps { 23 | config?: DisqusConfig; 24 | className?: string; 25 | placeholder?: string; 26 | } 27 | export class CommentCount extends React.Component {} 28 | 29 | export interface CommentEmbedProps { 30 | commentId: string; 31 | showParentComment?: boolean; 32 | showMedia?: boolean; 33 | width?: number; 34 | height?: number; 35 | } 36 | export class CommentEmbed extends React.Component {} 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Build 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x, 18.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: yarn 29 | - run: yarn add react react-dom 30 | - run: yarn build 31 | name: Build 32 | - run: yarn lint 33 | continue-on-error: true 34 | name: Lint 35 | - run: yarn test 36 | name: Tests 37 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Stale 2 | on: 3 | schedule: 4 | # At 10am every day 5 | - cron: '0 10 * * *' 6 | 7 | permissions: 8 | issues: write 9 | pull-requests: write 10 | 11 | jobs: 12 | stale: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/stale@v5 16 | with: 17 | days-before-stale: 30 18 | days-before-close: 7 19 | remove-stale-when-updated: true 20 | stale-issue-label: ":ghost: stale" 21 | stale-issue-message: "This issue has been automatically marked as stale because it appears to have gone quiet. It will be closed in 7 days if no further activity occurs. Thank you for your contributions." 22 | exempt-issue-labels: "pinned,enhancement,help wanted,confirmed,WIP" 23 | stale-pr-label: ":ghost: stale" 24 | stale-pr-message: "This pull request has been automatically marked as stale because it appears to have gone quiet. It will be closed in 7 days if no further activity occurs. Thank you for your contributions." 25 | exempt-pr-labels: "pinned,enhancement,WIP" 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Brett Stevenson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Motivation and Context 7 | 8 | 9 | 10 | ## How Has This Been Tested? 11 | 12 | 13 | 14 | 15 | ## Screenshots (if appropriate): 16 | 17 | ## Types of changes 18 | 19 | - [ ] Bug fix (non-breaking change which fixes an issue) 20 | - [ ] New feature (non-breaking change which adds functionality) 21 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 22 | 23 | ## Checklist: 24 | 25 | 26 | - [ ] My code follows the code style of this project. 27 | - [ ] My change requires a change to the documentation. 28 | - [ ] I have updated the documentation accordingly. 29 | - [ ] I have read the [**CONTRIBUTING.md**](./docs/CONTRIBUTING.md) document. 30 | - [ ] All new and existing tests passed. 31 | -------------------------------------------------------------------------------- /__tests__/CommentEmbed.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, cleanup } from '@testing-library/react'; 3 | // Components 4 | import CommentEmbed from '../src/components/CommentEmbed.jsx'; 5 | 6 | 7 | const commentConfig = { 8 | commentId: '4817304024', 9 | width: 680, 10 | height: 320, 11 | showMedia: true, 12 | showParentComment: false, 13 | className: 'embedded-comment', 14 | }; 15 | 16 | const getExpectedSrc = (config) => { 17 | const RADIX_BASE = 36; 18 | const post = Number(config.commentId).toString(RADIX_BASE); 19 | const parentParam = config.showParentComment ? '1' : '0'; 20 | const mediaParam = config.showMedia ? '1' : '0'; 21 | return `https://embed.disqus.com/p/${post}?p=${parentParam}&m=${mediaParam}`; 22 | }; 23 | 24 | const Component = (props) => ( 25 | 29 | ); 30 | 31 | // Cleanup tests to prevent memory leaks 32 | afterEach(cleanup); 33 | 34 | test('Has correct attributes', () => { 35 | const { getByTestId } = render(); 36 | // Check the iframe has the correct 'src' 37 | expect(getByTestId('comment-embed')).toHaveAttribute('src', getExpectedSrc(commentConfig)); 38 | // Check the correct 'width' is assigned 39 | expect(getByTestId('comment-embed')).toHaveAttribute('width', commentConfig.width.toString()); 40 | // Check the correct 'height' is assigned 41 | expect(getByTestId('comment-embed')).toHaveAttribute('height', commentConfig.height.toString()); 42 | // Check that the 'className' is assigned to the iframe 43 | expect(getByTestId('comment-embed')).toHaveAttribute('class', commentConfig.className); 44 | }); 45 | -------------------------------------------------------------------------------- /__tests__/Disqus.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, cleanup } from '@testing-library/react'; 3 | // Components 4 | import Disqus from '../src/components/Disqus.jsx'; 5 | 6 | 7 | const disqusConfig = { 8 | url: 'https://joy-of-testing.com/', 9 | title: 'Joy of Testing', 10 | identifier: 'tester', 11 | }; 12 | 13 | const Component = (props) => ( 14 | 18 | ); 19 | 20 | // Cleanup tests to prevent memory leaks 21 | afterEach(cleanup); 22 | 23 | test('Has correct attributes', () => { 24 | const { getByTestId } = render(); 25 | // Check that the correct ID is added 26 | expect(getByTestId('disqus-thread')).toHaveAttribute('id', 'disqus_thread'); 27 | }); 28 | 29 | test('Creates window.disqus_config', () => { 30 | render(); 31 | expect(global.window.disqus_config).toBeTruthy(); 32 | }); 33 | 34 | test('Inserts the script correctly', () => { 35 | const { baseElement } = render(); 36 | const scriptQuery = baseElement.querySelectorAll('#dsq-embed-scr'); 37 | // Make sure only one script is inserted 38 | expect(scriptQuery.length).toEqual(1); 39 | // Check that the script src is set correctly 40 | expect(scriptQuery[0].src).toEqual('https://testing.disqus.com/embed.js'); 41 | }); 42 | 43 | test('Cleans script and window attributes on unmount', () => { 44 | const { baseElement, unmount } = render(); 45 | unmount(); 46 | const scriptQuery = baseElement.querySelectorAll('#dsq-embed-scr'); 47 | // Make sure the script is removed 48 | expect(scriptQuery.length).toEqual(0); 49 | // Make sure window.DISQUS is removed 50 | expect(global.window.DISQUS).toBeUndefined(); 51 | }); 52 | -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTOR CODE OF CONDUCT 2 | 3 | As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. 6 | 7 | Examples of unacceptable behavior by participants include: 8 | 9 | * The use of sexualized language or imagery 10 | * Personal attacks 11 | * Trolling or insulting/derogatory comments 12 | * Public or private harassment 13 | * Publishing other’s private information, such as physical or electronic addresses, without explicit permission 14 | * Other unethical or unprofessional conduct 15 | 16 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. 17 | 18 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. 19 | 20 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 21 | 22 | This Code of Conduct is adapted from the Contributor Covenant, version 1.2.0, available [here](https://www.contributor-covenant.org/version/1/2/0/code-of-conduct.html). 23 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | export function insertScript(src, id, parent) { 5 | const script = window.document.createElement('script'); 6 | script.async = true; 7 | script.src = src; 8 | script.id = id; 9 | parent.appendChild(script); 10 | return script; 11 | } 12 | 13 | export function removeScript(id, parent) { 14 | const script = window.document.getElementById(id); 15 | if (script) { 16 | parent.removeChild(script); 17 | } 18 | } 19 | 20 | export function debounce(func, wait, runOnFirstCall) { 21 | let timeout; 22 | return function (...args) { 23 | const context = this; 24 | const deferredExecution = function () { 25 | timeout = null; 26 | if (!runOnFirstCall) { 27 | func.apply(context, args); 28 | } 29 | }; 30 | const callNow = runOnFirstCall && !timeout; 31 | window.clearTimeout(timeout); 32 | timeout = setTimeout(deferredExecution, wait); 33 | if (callNow) { 34 | func.apply(context, args); 35 | } 36 | }; 37 | } 38 | 39 | export function isReactElement(element) { 40 | if (React.isValidElement(element)) { 41 | return true; 42 | } else if (Array.isArray(element)) { 43 | return element.some((value) => React.isValidElement(value)); 44 | } 45 | return false; 46 | } 47 | 48 | export function shallowComparison(currentProps, nextProps) { 49 | // Perform a comparison of all props, excluding React Elements, to prevent 50 | // unnecessary updates 51 | const propNames = new Set(Object.keys(currentProps).concat(Object.keys(nextProps))); 52 | const changes = [].concat(...propNames).filter((name) => { 53 | if (typeof currentProps[name] === 'object') { 54 | if (shallowComparison(currentProps[name], nextProps[name])) { 55 | return true; 56 | } 57 | } else if (currentProps[name] !== nextProps[name] && !isReactElement(currentProps[name])) { 58 | return true; 59 | } 60 | return false; 61 | }); 62 | return changes.length !== 0; 63 | } 64 | -------------------------------------------------------------------------------- /src/components/CommentEmbed.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | 5 | export default class CommentEmbed extends React.Component { 6 | 7 | getSrc() { 8 | const RADIX_BASE = 36; 9 | const post = Number(this.props.commentId).toString(RADIX_BASE); 10 | const parentParam = this.props.showParentComment ? '1' : '0'; 11 | const mediaParam = this.props.showMedia ? '1' : '0'; 12 | return `https://embed.disqus.com/p/${post}?p=${parentParam}&m=${mediaParam}`; 13 | } 14 | 15 | render() { 16 | // eslint-disable-next-line no-unused-vars 17 | const { commentId, showMedia, showParentComment, ...props } = this.props; 18 | return ( 19 |