├── .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 |
--------------------------------------------------------------------------------