├── .nvmrc ├── .husky ├── .gitignore └── pre-commit ├── .node-version ├── .env.example ├── .eslintignore ├── .prettierignore ├── example ├── types │ └── react-ghfork │ │ └── index.d.ts ├── env.ts ├── index.tsx ├── App.css └── App.tsx ├── renovate.json ├── .prettierrc ├── src ├── global.d.ts └── index.tsx ├── .lintstagedrc.json ├── tsconfig.es.json ├── scripts └── build_demo.sh ├── jest.config.json ├── test ├── __snapshots__ │ └── index.test.tsx.snap └── index.test.tsx ├── .eslintrc.js ├── .babelrc ├── .circleci ├── publish_and_tagging.sh └── config.yml ├── .github └── workflows │ └── github-release.yml ├── webpack.config.dev.ts ├── tsconfig.demo.json ├── tsconfig.json ├── .gitignore ├── README.md ├── webpack.config.ts └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 14.17.3 2 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 14.17.3 2 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | INSTAGRAM_ACCESS_TOKEN= 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | es 4 | lib 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | es 4 | lib 5 | -------------------------------------------------------------------------------- /example/types/react-ghfork/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-ghfork'; 2 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@sugarshin:js-lib" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint-staged 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "semi": true, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | instgrm: { 3 | Embeds: { 4 | process(): void; 5 | }; 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.{ts,js}": [ 3 | "npm run fix:eslint", 4 | "npm run fix:prettier", 5 | "git add" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.es.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "outDir": "./es", 5 | "module": "es2015", 6 | "moduleResolution": "node" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /scripts/build_demo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | main() { 4 | local -r dir=build 5 | npm run clean:build 6 | mkdir -p "${dir}" 7 | curl https://sugarshin.net/favicon.ico > "${dir}"/favicon.ico 8 | webpack 9 | } 10 | 11 | main 12 | -------------------------------------------------------------------------------- /example/env.ts: -------------------------------------------------------------------------------- 1 | const { INSTAGRAM_ACCESS_TOKEN } = process.env; 2 | 3 | if (!INSTAGRAM_ACCESS_TOKEN) { 4 | throw new TypeError('INSTAGRAM_ACCESS_TOKEN must be required'); 5 | } 6 | 7 | export default { 8 | INSTAGRAM_ACCESS_TOKEN, 9 | }; 10 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest", 3 | "testRegex": "((\\.|/)test)\\.tsx$", 4 | "testEnvironment": "jsdom", 5 | "moduleFileExtensions": [ 6 | "ts", 7 | "tsx", 8 | "js", 9 | "jsx", 10 | "json" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should render self and sub-components 1`] = ` 4 |
11 | `; 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint'], 5 | env: { 6 | node: true, 7 | }, 8 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react/recommended'], 9 | settings: { 10 | react: { 11 | version: 'detect', 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /test/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import InstagramEmbed from '../src'; 4 | 5 | test('should render self and sub-components', () => { 6 | const tree = renderer 7 | .create() 8 | .toJSON(); 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/env", { "modules": false }], 4 | "@babel/react", 5 | "@babel/typescript" 6 | ], 7 | "plugins": [ 8 | "react-hot-loader/babel", 9 | "@babel/plugin-proposal-class-properties" 10 | ], 11 | "env": { 12 | "test": { 13 | "presets": ["@babel/env"] 14 | }, 15 | "commonjs": { 16 | "presets": ["@babel/env"] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.circleci/publish_and_tagging.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | PACKAGE_NAME=$(cat < ./package.json | jq .name -r) 4 | NEW_VERSION=$(cat < ./package.json | jq .version -r) 5 | LATEST_VERSION=$(npm v "${PACKAGE_NAME}" version 2>/dev/null || exit 0) 6 | 7 | if [[ "$LATEST_VERSION" = "$NEW_VERSION" ]]; then 8 | echo "${NEW_VERSION} exists. It was skip publishing." 9 | else 10 | npm publish 11 | TAG=v${NEW_VERSION} 12 | git tag "${TAG}" 13 | git push origin "${TAG}" 14 | fi 15 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | const main = () => { 6 | const meta = document.createElement('meta'); 7 | meta.setAttribute('name', 'viewport'); 8 | meta.setAttribute('content', 'width=device-width, initial-scale=1'); 9 | document.head.appendChild(meta); 10 | 11 | const root = document.body.appendChild(document.createElement('div')); 12 | ReactDOM.render(, root); 13 | }; 14 | 15 | main(); 16 | -------------------------------------------------------------------------------- /.github/workflows/github-release.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | create-release: 10 | name: Create Release 11 | runs-on: ubuntu-20.04 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Create Release 15 | id: create_release 16 | uses: actions/create-release@v1 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | with: 20 | tag_name: ${{ github.ref }} 21 | release_name: Release ${{ github.ref }} 22 | -------------------------------------------------------------------------------- /webpack.config.dev.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from 'webpack-dev-server'; 2 | import baseConfig from './webpack.config'; 3 | 4 | const devServer: Configuration = { 5 | contentBase: './example', 6 | hot: true, 7 | publicPath: '/', 8 | host: '0.0.0.0', 9 | port: Number(process.env.PORT || '8080'), 10 | before(app) { 11 | app.get('/favicon.ico', (_, res) => res.status(200).send()); 12 | }, 13 | }; 14 | 15 | const config = { 16 | ...baseConfig, 17 | resolve: { 18 | ...baseConfig.resolve, 19 | alias: { 20 | ...baseConfig.resolve?.alias, 21 | 'react-dom': '@hot-loader/react-dom', 22 | }, 23 | }, 24 | devServer, 25 | }; 26 | 27 | export default config; 28 | -------------------------------------------------------------------------------- /tsconfig.demo.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "outDir": "./example", 5 | "target": "es2018", 6 | "declaration": false, 7 | "jsx": "react", 8 | "lib": ["es2015", "es2016", "es2017", "dom", "dom.iterable", "esnext.asynciterable"], 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "allowJs": true, 12 | "sourceMap": false, 13 | "suppressImplicitAnyIndexErrors": true, 14 | "allowSyntheticDefaultImports": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "react-instagram-embed": ["src/index"] 18 | }, 19 | "typeRoots": [ 20 | "node_modules/@types", 21 | "example/types" 22 | ] 23 | }, 24 | "exclude": ["node_modules", "src/**/*.test.ts", "src/**/*.test.tsx"] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "declaration": true, 5 | "strict": true, 6 | "pretty": true, 7 | "noImplicitAny": true, 8 | "jsx": "react", 9 | "lib": ["es2015", "dom"], 10 | "strictNullChecks": true, 11 | "skipLibCheck": true, 12 | "strictFunctionTypes": true, 13 | "strictPropertyInitialization": true, 14 | "noImplicitThis": true, 15 | "alwaysStrict": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "esModuleInterop": true, 21 | "resolveJsonModule": true 22 | }, 23 | "include": ["src/**/*.ts", "src/**/*.tsx"], 24 | "exclude": ["node_modules", "lib", "es", "**/*.test.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /example/App.css: -------------------------------------------------------------------------------- 1 | html, :root { 2 | font-size: 62.5%; 3 | } 4 | html, :root, body { 5 | background-color: #fafafa; 6 | height: 100%; 7 | font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Roboto, "游ゴシック体", "Yu Gothic", YuGothic, "Hiragino Kaku Gothic ProN", "ヒラギノ角ゴ ProN", "Hiragino Kaku Gothic Pro", Meiryo, "メイリオ", "Noto Sans Japanese", sans-serif; 8 | } 9 | body { 10 | color: #323b43; 11 | line-height: 1.6; 12 | font-size: 1.4rem; 13 | } 14 | h1 { 15 | margin-top: 0; 16 | text-align: center; 17 | } 18 | .body { 19 | margin: 0 auto; 20 | padding: 1em .5em; 21 | } 22 | .instagram-embed { 23 | min-height: 390px; 24 | } 25 | .instagram-embed + .ui { 26 | margin-top: 1em; 27 | } 28 | .ui { 29 | margin-top: .5em; 30 | } 31 | .ui input[type=number] { 32 | max-width: 128px; 33 | margin-right: 1em; 34 | } 35 | .ui-label { 36 | margin-right: 1em; 37 | } 38 | pre { 39 | background-color: #eee; 40 | padding: 1em; 41 | } 42 | .hljs { 43 | background-color: #eee; 44 | } 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/4bff4a2986af526650f1d329d97047dc1fa87599/Node.gitignore 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # Typescript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # next.js build output 63 | .next 64 | 65 | lib 66 | es 67 | build 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-instagram-embed 2 | 3 | [![CircleCI][circleci-image]][circleci-url] 4 | [![npm version][npm-image]][npm-url] 5 | [![License][license-image]][license-url] 6 | 7 | React embedding Instagram posts component 8 | 9 | ```bash 10 | yarn add react-instagram-embed 11 | 12 | # or 13 | 14 | npm i react-instagram-embed 15 | ``` 16 | 17 | [Live demo](https://sugarshin.github.io/react-instagram-embed/) 18 | 19 | ## Usage 20 | 21 | ```tsx 22 | import InstagramEmbed from 'react-instagram-embed'; 23 | 24 | {}} 33 | onSuccess={() => {}} 34 | onAfterRender={() => {}} 35 | onFailure={() => {}} 36 | /> 37 | ``` 38 | 39 | Access token is combination of App Id and Client Token. See https://developers.facebook.com/docs/instagram/oembed/#access-tokens for more details. 40 | 41 | ## props 42 | 43 | - `url` {String} Instagram URL. Required 44 | - `clientAccessToken` {String} Instagram Client Access Token. Required 45 | - `maxWidth` {Number} Max width. Minimum size is `320`. Default `undefined` 46 | - `hideCaption` {Boolean} Default `false` 47 | - `containerTagName` {String} Default `'div'` 48 | - `protocol` {String} Instagram API script protocol. Default `''` same as current protocol 49 | - This property needed if you use device's file system, for example, Electron, Cordova apps 50 | - `injectScript` {Boolean} Default `true` 51 | - `onLoading` {Function} 52 | - `onSuccess` {Function} 53 | - `onAfterRender` {Function} 54 | - `onFailure` {Function} 55 | 56 | ## License 57 | 58 | [MIT][license-url] 59 | 60 | © sugarshin 61 | 62 | [npm-image]: https://img.shields.io/npm/v/react-instagram-embed.svg?style=flat-square 63 | [npm-url]: https://www.npmjs.org/package/react-instagram-embed 64 | [circleci-image]: https://circleci.com/gh/sugarshin/react-instagram-embed/tree/master.svg?style=svg&circle-token=8991301b0db526852c58dc884a349cb8e00f1b24 65 | [circleci-url]: https://circleci.com/gh/sugarshin/react-instagram-embed/tree/master 66 | [license-image]: https://img.shields.io/:license-mit-blue.svg?style=flat-square 67 | [license-url]: https://sugarshin.mit-license.org/ 68 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack, { Configuration } from 'webpack'; 3 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 4 | import DotenvWebpack from 'dotenv-webpack'; 5 | import pkgjson from './package.json'; 6 | 7 | const { NODE_ENV = 'development' } = process.env; 8 | const prod = NODE_ENV === 'production'; 9 | 10 | const htmlWebpackPluginConfig = { 11 | title: `${pkgjson.name} | ${pkgjson.description}`, 12 | minify: { collapseWhitespace: true }, 13 | favicon: 'build/favicon.ico', 14 | }; 15 | 16 | const plugins: Configuration['plugins'] = [new HtmlWebpackPlugin(prod ? htmlWebpackPluginConfig : undefined)]; 17 | 18 | if (prod) { 19 | plugins.push(new webpack.EnvironmentPlugin(['INSTAGRAM_ACCESS_TOKEN'])); 20 | } else { 21 | plugins.push(new webpack.HotModuleReplacementPlugin(), new DotenvWebpack()); 22 | } 23 | 24 | function mode(): Configuration['mode'] { 25 | if (NODE_ENV === 'development' || NODE_ENV === 'production') { 26 | return NODE_ENV; 27 | } 28 | return 'none'; 29 | } 30 | 31 | const config: Configuration = { 32 | mode: mode(), 33 | plugins, 34 | entry: './example/index.tsx', 35 | cache: true, 36 | output: { 37 | path: path.resolve(__dirname, 'build'), 38 | filename: '[name].js', 39 | }, 40 | resolve: { 41 | extensions: ['.ts', '.tsx', '.js', '.json', '.mjs'], 42 | alias: { 43 | [pkgjson.name]: path.resolve(__dirname, 'src/index.tsx'), 44 | }, 45 | }, 46 | module: { 47 | rules: [ 48 | { 49 | test: /\.(j|t)sx?$/, 50 | exclude: /node_modules/, 51 | use: [ 52 | { 53 | loader: 'babel-loader', 54 | options: { plugins: ['react-hot-loader/babel'] }, 55 | }, 56 | ], 57 | }, 58 | { 59 | test: /\.css$/, 60 | use: ['style-loader', { loader: 'css-loader' }], 61 | }, 62 | ], 63 | }, 64 | ...(prod 65 | ? { 66 | optimization: { 67 | splitChunks: { 68 | maxSize: 244000, 69 | cacheGroups: { 70 | vendor: { 71 | test: /node_modules/, 72 | name: 'vendor', 73 | chunks: 'initial', 74 | enforce: true, 75 | }, 76 | }, 77 | }, 78 | }, 79 | } 80 | : {}), 81 | }; 82 | 83 | export default config; 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-instagram-embed", 3 | "version": "3.0.0", 4 | "description": "React embedding Instagram posts component", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "jsnext:main": "es/index.js", 8 | "files": [ 9 | "lib", 10 | "es" 11 | ], 12 | "keywords": [ 13 | "react", 14 | "component", 15 | "instagram" 16 | ], 17 | "scripts": { 18 | "prepare": "npx is-ci || husky install", 19 | "lint-staged": "lint-staged", 20 | "lint:eslint": "eslint '**/*.{ts,tsx,js}'", 21 | "fix:eslint": "npm run lint:eslint -- --fix", 22 | "_prettier": "prettier '**/*.{ts,tsx,js}'", 23 | "lint:prettier": "npm run _prettier -- --check", 24 | "fix:prettier": "npm run _prettier -- --write", 25 | "typecheck": "tsc --noEmit", 26 | "dev": "webpack serve --config webpack.config.dev.ts", 27 | "clean:lib": "if [ -d lib ]; then rm -rf lib; fi", 28 | "clean:es": "if [ -d es ]; then rm -rf es; fi", 29 | "precompile:commonjs": "npm run clean:lib", 30 | "compile:commonjs": "tsc --outDir lib --module commonjs", 31 | "precompile:es": "npm run clean:es", 32 | "compile:es": "tsc --build tsconfig.es.json", 33 | "compile": "npm run compile:commonjs && npm run compile:es", 34 | "test": "jest --config jest.config.json", 35 | "test:coverage": "npm test -- --coverage", 36 | "clean:build": "if [ -d build ]; then rm -rf build; fi", 37 | "build:demo": "./scripts/build_demo.sh" 38 | }, 39 | "repository": { 40 | "type": "git", 41 | "url": "git+https://github.com/sugarshin/react-instagram-embed.git" 42 | }, 43 | "author": "sugarshin", 44 | "license": "MIT", 45 | "bugs": { 46 | "url": "https://github.com/sugarshin/react-instagram-embed/issues" 47 | }, 48 | "homepage": "https://github.com/sugarshin/react-instagram-embed#readme", 49 | "peerDependencies": { 50 | "react": "^17.0.0 || ^16.0.0 || ^15.0.0 || ~0.14.0", 51 | "react-dom": "^17.0.0 || ^16.0.0 || ^15.0.0 || ~0.14.0" 52 | }, 53 | "devDependencies": { 54 | "@babel/core": "7.26.9", 55 | "@babel/plugin-proposal-class-properties": "7.18.6", 56 | "@babel/preset-env": "7.26.9", 57 | "@babel/preset-react": "7.26.3", 58 | "@babel/preset-typescript": "7.26.0", 59 | "@hot-loader/react-dom": "17.0.2", 60 | "@types/dotenv-webpack": "7.0.8", 61 | "@types/jest": "26.0.24", 62 | "@types/node": "14.18.63", 63 | "@types/react": "17.0.83", 64 | "@types/react-dom": "17.0.26", 65 | "@types/react-test-renderer": "17.0.9", 66 | "@types/webpack": "5.28.5", 67 | "@types/webpack-dev-server": "3.11.6", 68 | "@typescript-eslint/eslint-plugin": "4.33.0", 69 | "@typescript-eslint/parser": "4.33.0", 70 | "babel-loader": "8.4.1", 71 | "css-loader": "5.2.7", 72 | "dotenv-webpack": "7.1.1", 73 | "eslint": "7.32.0", 74 | "eslint-plugin-react": "7.37.4", 75 | "highlight.js": "11.11.1", 76 | "html-webpack-plugin": "5.6.3", 77 | "husky": "7.0.4", 78 | "is-ci": "3.0.1", 79 | "jest": "27.5.1", 80 | "lint-staged": "11.2.6", 81 | "normalize.css": "8.0.1", 82 | "prettier": "2.8.8", 83 | "react": "17.0.2", 84 | "react-dom": "17.0.2", 85 | "react-github-ribbons": "0.2.2", 86 | "react-hot-loader": "4.13.1", 87 | "react-test-renderer": "17.0.2", 88 | "style-loader": "3.3.4", 89 | "ts-jest": "27.1.5", 90 | "ts-node": "10.9.2", 91 | "typescript": "4.9.5", 92 | "webpack": "5.98.0", 93 | "webpack-cli": "4.10.0", 94 | "webpack-dev-server": "3.11.3" 95 | }, 96 | "dependencies": { 97 | "type-fest": "^1.2.2" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /example/App.tsx: -------------------------------------------------------------------------------- 1 | import 'normalize.css/normalize.css'; 2 | import 'highlight.js/styles/github.css'; 3 | import './App.css'; 4 | import React, { Component } from 'react'; 5 | import { hot } from 'react-hot-loader'; 6 | import GitHubRibbon from 'react-github-ribbons'; 7 | import InstagramEmbed from 'react-instagram-embed'; 8 | import hljs from 'highlight.js'; 9 | import env from './env'; 10 | 11 | interface State { 12 | url: string; 13 | maxWidth?: number; 14 | hideCaption: boolean; 15 | } 16 | 17 | class App extends Component, State> { 18 | public state = { url: urls[0], maxWidth: 375, hideCaption: false }; 19 | 20 | private numberInputRef = React.createRef(); 21 | 22 | public componentDidMount() { 23 | this.highlight(); 24 | } 25 | 26 | public componentDidUpdate() { 27 | this.highlight(); 28 | } 29 | 30 | public render() { 31 | const { url, maxWidth, hideCaption } = this.state; 32 | return ( 33 |
34 | 35 |
41 |

React Instagram Embed

42 | 49 |
50 | Hide caption 51 | 52 |
53 |
54 | Max width 55 | 56 | 57 |
58 |
59 | Select photo 60 | 67 |
68 |
 69 |             {getCode(url, maxWidth, hideCaption)}
 70 |           
71 |
72 |
73 | ); 74 | } 75 | 76 | private highlight() { 77 | [...document.querySelectorAll('pre code')].forEach((el) => hljs.highlightElement(el as HTMLElement)); 78 | } 79 | 80 | private handleMaxWidthChange = () => { 81 | const maxWidth = this.numberInputRef.current.value ? parseInt(this.numberInputRef.current.value, 10) : undefined; 82 | console.log('maxWidth', maxWidth); 83 | this.setState({ maxWidth }); 84 | }; 85 | 86 | private hanldeURLSelect = (e: React.SyntheticEvent) => { 87 | this.setState({ url: e.currentTarget.value }); 88 | }; 89 | 90 | private handleCaptionChange = () => { 91 | this.setState({ hideCaption: !this.state.hideCaption }); 92 | }; 93 | } 94 | 95 | const urls = [ 96 | 'https://instagr.am/p/Zw9o4/', 97 | 'https://instagr.am/p/Ytlfl/', 98 | 'https://instagr.am/p/Zn1Xz/', 99 | 'https://instagr.am/p/HLLj2RgURT/', 100 | 'https://instagr.am/p/HeZ7IxgUUc/', 101 | 'https://instagr.am/p/LJ2tq9AUaO/', 102 | ]; 103 | 104 | const getCode = (url: string, maxWidth: number, hideCaption: boolean) => ` {}} 113 | onSuccess={() => {}} 114 | onAfterRender={() => {}} 115 | onFailure={() => {}} 116 | />`; 117 | 118 | export default hot(module)(App); 119 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | gh-pages: sugarshin/gh-pages@0.0.6 5 | 6 | references: 7 | workspace_root: &workspace_root ~/workspace 8 | NODE_MODULES_CACHE_KEY: &NODE_MODULES_CACHE_KEY v1-dependencies-{{ arch }}-{{ .Branch }}-{{ .Environment.CIRCLE_WORKFLOW_WORKSPACE_ID }}-{{ checksum "package-lock.json" }}-node-modules 9 | 10 | executors: 11 | nodejs: 12 | working_directory: *workspace_root 13 | docker: 14 | - image: cimg/node:14.17.3 15 | 16 | commands: 17 | restore_npm_cache: 18 | steps: 19 | - restore_cache: 20 | name: Restore npm cache 21 | keys: 22 | - v1-npm-cache-{{ checksum "package-lock.json" }} 23 | - v1-npm-cache- 24 | save_npm_cache: 25 | steps: 26 | - save_cache: 27 | name: Save npm cache 28 | key: v1-npm-cache-{{ checksum "package-lock.json" }} 29 | paths: 30 | - ~/.npm 31 | restore_node_modules: 32 | steps: 33 | - restore_cache: 34 | name: Restore node_modules 35 | keys: 36 | - *NODE_MODULES_CACHE_KEY 37 | save_node_modules: 38 | steps: 39 | - save_cache: 40 | name: Save node_modules 41 | key: *NODE_MODULES_CACHE_KEY 42 | paths: 43 | - node_modules 44 | add_npmrc: 45 | steps: 46 | - run: 47 | name: Authenticate with registry 48 | command: echo "//registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN}" >> ~/.npmrc 49 | 50 | jobs: 51 | test: 52 | executor: 53 | name: nodejs 54 | steps: 55 | - checkout 56 | - restore_npm_cache 57 | - run: npm ci 58 | - save_npm_cache 59 | - save_node_modules 60 | - run: 61 | name: Lint 62 | command: | 63 | npm run lint:eslint 64 | npm run lint:prettier 65 | - run: 66 | name: Typecheck 67 | command: npm run typecheck 68 | - run: 69 | name: Test 70 | command: npm test 71 | 72 | build: 73 | executor: 74 | name: nodejs 75 | steps: 76 | - checkout 77 | - restore_node_modules 78 | - run: 79 | name: Compile 80 | command: npm run compile 81 | environment: 82 | NODE_ENV: production 83 | - store_artifacts: 84 | path: lib 85 | - store_artifacts: 86 | path: es 87 | - persist_to_workspace: 88 | root: *workspace_root 89 | paths: 90 | - lib 91 | - es 92 | 93 | build_demo: 94 | executor: 95 | name: nodejs 96 | steps: 97 | - checkout 98 | - restore_node_modules 99 | - run: 100 | name: Build Demo 101 | command: npm run build:demo 102 | environment: 103 | NODE_ENV: production 104 | - store_artifacts: 105 | path: build 106 | - persist_to_workspace: 107 | root: *workspace_root 108 | paths: 109 | - build 110 | 111 | publish: 112 | executor: 113 | name: nodejs 114 | steps: 115 | - checkout 116 | - attach_workspace: 117 | at: *workspace_root 118 | - add_ssh_keys: 119 | fingerprints: 120 | - 'e4:52:fe:79:78:b0:cc:37:b6:3a:dd:71:8b:ca:49:9c' 121 | - add_npmrc 122 | - run: 123 | name: Publish package to npm and tagging 124 | command: /bin/bash .circleci/publish_and_tagging.sh 125 | 126 | workflows: 127 | test_build_deploy_publish: 128 | jobs: 129 | - test 130 | - build: 131 | requires: 132 | - test 133 | - build_demo: 134 | requires: 135 | - test 136 | - gh-pages/deploy: 137 | executor: 138 | name: nodejs 139 | build-dir: build 140 | workspace-root: *workspace_root 141 | ssh-fingerprints: 'e4:52:fe:79:78:b0:cc:37:b6:3a:dd:71:8b:ca:49:9c' 142 | git-user: CircleCI 143 | git-email: s+circleci@sugarshin.net 144 | requires: 145 | - build_demo 146 | filters: 147 | branches: 148 | only: master 149 | - publish: 150 | requires: 151 | - build 152 | filters: 153 | branches: 154 | only: master 155 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { stringify } from 'querystring'; 3 | import { SetRequired } from 'type-fest'; 4 | 5 | interface DefaultProps { 6 | hideCaption: boolean; 7 | containerTagName: T; 8 | protocol: string; 9 | injectScript: boolean; 10 | } 11 | 12 | type Html = string; 13 | 14 | interface Response { 15 | html: Html; 16 | } 17 | 18 | interface State { 19 | html: Html | null; 20 | } 21 | 22 | interface RequestPromise { 23 | promise: Promise; 24 | cancel(): void; 25 | } 26 | 27 | type PropsInternal = SetRequired; 28 | 29 | export interface Props extends Partial> { 30 | url: string; 31 | clientAccessToken: string; 32 | maxWidth?: number; 33 | className?: string; 34 | onLoading?(): void; 35 | onSuccess?(response: Response): void; 36 | onAfterRender?(): void; 37 | onFailure?(error: Error): void; 38 | } 39 | 40 | export default class InstagramEmbed extends React.Component { 41 | public static defaultProps: DefaultProps = { 42 | hideCaption: false, 43 | containerTagName: 'div', 44 | protocol: 'https:', 45 | injectScript: true, 46 | }; 47 | 48 | private request: RequestPromise | null = null; 49 | private timer?: number; 50 | 51 | constructor(props: PropsInternal) { 52 | super(props); 53 | this.state = { html: null }; 54 | } 55 | 56 | componentDidMount(): void { 57 | if (window.instgrm) { 58 | this.fetchEmbed(this.getQueryParams(this.props)); 59 | } else { 60 | if (this.props.injectScript && !document.getElementById('react-instagram-embed-script')) { 61 | this.injectScript(); 62 | } 63 | this.checkAPI().then(() => { 64 | this.fetchEmbed(this.getQueryParams(this.props)); 65 | }); 66 | } 67 | } 68 | 69 | componentDidUpdate(prevProps: PropsInternal): void { 70 | const { url, clientAccessToken, hideCaption, maxWidth, containerTagName, className } = this.props; 71 | if ( 72 | prevProps.url !== url || 73 | prevProps.clientAccessToken !== clientAccessToken || 74 | prevProps.hideCaption !== hideCaption || 75 | prevProps.maxWidth !== maxWidth || 76 | prevProps.containerTagName !== containerTagName || 77 | prevProps.className !== className 78 | ) { 79 | (this.request as RequestPromise).cancel(); 80 | this.fetchEmbed(this.getQueryParams(this.props)); 81 | } 82 | } 83 | 84 | componentWillUnmount(): void { 85 | this.cancel(); 86 | } 87 | 88 | public render(): React.ReactNode { 89 | const Element = this.props.containerTagName; 90 | return ; 91 | } 92 | 93 | // Public 94 | public cancel = (): void => { 95 | if (this.request) { 96 | this.request.cancel(); 97 | } 98 | }; 99 | 100 | private fetchEmbed(queryParams: string): void { 101 | this.request = this.createRequestPromise(`https://graph.facebook.com/v11.0/instagram_oembed/?${queryParams}`); 102 | 103 | if (this.props.onLoading) { 104 | this.props.onLoading(); 105 | } 106 | 107 | this.request.promise.then(this.handleFetchSuccess).catch(this.handleFetchFailure); 108 | } 109 | 110 | private omitComponentProps() { 111 | const { 112 | /* eslint-disable @typescript-eslint/no-unused-vars */ 113 | url, 114 | clientAccessToken, 115 | hideCaption, 116 | maxWidth, 117 | containerTagName, 118 | onLoading, 119 | onSuccess, 120 | onAfterRender, 121 | onFailure, 122 | protocol, 123 | injectScript, 124 | /* eslint-enable @typescript-eslint/no-unused-vars */ 125 | ...rest 126 | } = this.props; 127 | return rest; 128 | } 129 | 130 | private injectScript(): void { 131 | const protocolToUse: string = window.location.protocol.indexOf('file') === 0 ? this.props.protocol : ''; 132 | 133 | const s = document.createElement('script'); 134 | s.async = s.defer = true; 135 | s.src = `${protocolToUse}//platform.instagram.com/en_US/embeds.js`; 136 | s.id = 'react-instagram-embed-script'; 137 | const body: HTMLElement | null = document.body; 138 | if (body) { 139 | body.appendChild(s); 140 | } 141 | } 142 | 143 | private checkAPI(): Promise { 144 | return new Promise((resolve) => { 145 | (function checkAPI(self: InstagramEmbed) { 146 | self.timer = window.setTimeout(() => { 147 | if (window.instgrm) { 148 | clearTimeout(self.timer); 149 | resolve(); 150 | } else { 151 | checkAPI(self); 152 | } 153 | }, 20); 154 | })(this); 155 | }); 156 | } 157 | 158 | private getQueryParams({ url, hideCaption, maxWidth }: PropsInternal): string { 159 | const query: { url: string; hidecaption: boolean; omitscript: true; fields: 'html'; maxwidth?: number } = { 160 | url, 161 | hidecaption: hideCaption, 162 | omitscript: true, 163 | fields: 'html', 164 | }; 165 | 166 | // "The request parameter 'maxwidth' must be an integer between 320 and 658." 167 | if (typeof maxWidth === 'number' && 320 <= maxWidth && maxWidth <= 658) { 168 | query.maxwidth = maxWidth; 169 | } 170 | 171 | return stringify(query); 172 | } 173 | 174 | private handleFetchSuccess = (response: Response): void => { 175 | if (this.props.onSuccess) { 176 | this.props.onSuccess(response); 177 | } 178 | 179 | this.setState({ html: response.html }, () => { 180 | window.instgrm.Embeds.process(); 181 | if (this.props.onAfterRender) { 182 | this.props.onAfterRender(); 183 | } 184 | }); 185 | }; 186 | 187 | private handleFetchFailure = (error: Error): void => { 188 | clearTimeout(this.timer); 189 | if (this.props.onFailure) { 190 | this.props.onFailure(error); 191 | } 192 | }; 193 | 194 | private createRequestPromise = (url: string): RequestPromise => { 195 | const request = {} as RequestPromise; 196 | 197 | request.promise = new Promise((resolve, reject) => { 198 | const promise = fetch(url, { 199 | headers: { 200 | Authorization: `Bearer ${this.props.clientAccessToken}`, 201 | }, 202 | }) 203 | .then((response) => response.json()) 204 | .then((json) => resolve(json)) 205 | .catch((err) => reject(err)); 206 | 207 | request.cancel = () => reject(new Error('Cancelled')); 208 | return promise; 209 | }); 210 | 211 | return request; 212 | }; 213 | } 214 | --------------------------------------------------------------------------------