├── .npmignore
├── .eslintignore
├── .gitignore
├── .eslintrc
├── .travis.yml
├── .babelrc
├── test
├── .eslintrc
├── mocha.opts
├── helpers
│ ├── setup.js
│ └── render.js
└── SoundCloud-test.js
├── example
├── webpack.config.js
├── components
│ ├── OptionsInput.js
│ ├── CustomWidget.js
│ └── OptionsTable.js
├── index.html
└── index.js
├── src
├── lib
│ └── createWidget.js
└── SoundCloud.js
├── LICENSE
├── package.json
└── README.md
/.npmignore:
--------------------------------------------------------------------------------
1 | dist/__tests__
2 | src
3 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | dist
4 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-airbnb"
3 | }
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "4.2.1"
4 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-0", "react"]
3 | }
4 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | test/*-test.js
2 | --compilers js:babel-core/register
3 | --require ./test/helpers/setup.js
4 |
--------------------------------------------------------------------------------
/test/helpers/setup.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Module dependencies
3 | */
4 |
5 | import { jsdom } from 'jsdom';
6 |
7 | global.document = jsdom('
');
8 | global.window = document.defaultView;
9 | global.navigator = global.window.navigator;
10 |
--------------------------------------------------------------------------------
/example/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | entry: `${__dirname}/index.js`,
3 | output: {
4 | path: __dirname,
5 | filename: 'bundle.js',
6 | publicPath: '/',
7 | },
8 |
9 | module: {
10 | loaders: [
11 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel'},
12 | ],
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/src/lib/createWidget.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Module dependencies
3 | */
4 |
5 | import load from 'load-script';
6 |
7 | /**
8 | * Create a new widget by requesting and using the SoundCloud Widget API.
9 | *
10 | * @param {String} id - reference to iframe element for widget
11 | * @param {Function} cb
12 | */
13 |
14 | const createWidget = (id, cb) => {
15 | // load the API, it's namespaced as `window.SC`
16 | return load('https://w.soundcloud.com/player/api.js', () => {
17 | return cb(window.SC.Widget(id)); // eslint-disable-line new-cap
18 | });
19 | };
20 |
21 | /**
22 | * Expose `createWidget`
23 | */
24 |
25 | export default createWidget;
26 |
--------------------------------------------------------------------------------
/example/components/OptionsInput.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | TextField,
4 | } from 'material-ui';
5 |
6 | export default class OptionsInput extends React.Component {
7 | static propTypes = {
8 | default: React.PropTypes.string.isRequired,
9 | type: React.PropTypes.string.isRequired,
10 | onChange: React.PropTypes.func.isRequired,
11 | };
12 |
13 | onSubmit(event) {
14 | event.preventDefault();
15 | this.props.onChange(this.refs.input.getValue());
16 | }
17 |
18 | render() {
19 | return (
20 |
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/example/components/CustomWidget.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SoundCloud from '../../';
3 |
4 | export default class CustomWidget extends React.Component {
5 | static propTypes = {
6 | url: React.PropTypes.string.isRequired,
7 | id: React.PropTypes.string.isRequired,
8 | opts: React.PropTypes.array.isRequired,
9 | };
10 |
11 | componentDidUpdate() {
12 | // otherwise it would only update when `url` changes.
13 | this.refs.widget.forceUpdate();
14 | }
15 |
16 | render() {
17 | const opts = this.props.opts.reduce((all, param) => {
18 | return {
19 | ...all,
20 | [param.name]: param.toggled,
21 | };
22 | }, {});
23 |
24 | return (
25 |
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 troy betz
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 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | react-soundcloud-widget
7 |
8 |
9 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/example/components/OptionsTable.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Table,
4 | TableHeader,
5 | TableHeaderColumn,
6 | TableBody,
7 | TableRow,
8 | TableRowColumn,
9 | } from 'material-ui';
10 |
11 | export default class OptionsTable extends React.Component {
12 | static propTypes = {
13 | opts: React.PropTypes.array.isRequired,
14 | onChange: React.PropTypes.func.isRequired,
15 | };
16 |
17 | onRowSelection(selectedIndices) {
18 | const selectedOpts = this.props.opts.map((opt, idx) => ({
19 | ...opt,
20 | toggled: selectedIndices.indexOf(idx) > -1 ? true : false,
21 | }));
22 |
23 | this.props.onChange(selectedOpts);
24 | }
25 |
26 | render() {
27 | return (
28 |
31 |
32 |
33 | Parameter
34 | Purpose
35 |
36 |
37 |
38 | {
39 | this.props.opts.map(opt =>
40 |
41 | {opt.name}
42 | {opt.purpose}
43 |
44 | )
45 | }
46 |
47 |
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-soundcloud-widget",
3 | "version": "2.0.4",
4 | "description": "react.js powered SoundCloud player component",
5 | "main": "dist/SoundCloud.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/troybetz/react-soundcloud-widget.git"
9 | },
10 | "keywords": [
11 | "soundcloud",
12 | "player",
13 | "widget",
14 | "react",
15 | "react-component"
16 | ],
17 | "author": "troy betz",
18 | "license": "MIT",
19 | "bugs": {
20 | "url": "https://github.com/troybetz/react-soundcloud-widget/issues"
21 | },
22 | "homepage": "https://github.com/troybetz/react-soundcloud-widget",
23 | "dependencies": {
24 | "load-script": "^1.0.0"
25 | },
26 | "devDependencies": {
27 | "babel-cli": "^6.2.0",
28 | "babel-core": "^6.2.1",
29 | "babel-eslint": "^4.1.3",
30 | "babel-loader": "^6.2.0",
31 | "babel-preset-es2015": "^6.1.18",
32 | "babel-preset-react": "^6.1.18",
33 | "babel-preset-stage-0": "^6.1.18",
34 | "eslint": "^1.7.3",
35 | "eslint-config-airbnb": "^0.1.0",
36 | "eslint-plugin-react": "^3.6.3",
37 | "expect": "^1.12.2",
38 | "jsdom": "^7.0.2",
39 | "material-ui": "^0.13.3",
40 | "mocha": "^2.3.3",
41 | "proxyquire": "^1.7.3",
42 | "react": "^0.14.0",
43 | "react-addons-test-utils": "^0.14.0",
44 | "react-dom": "^0.14.0",
45 | "react-tap-event-plugin": "^0.2.1",
46 | "webpack": "^1.12.8"
47 | },
48 | "peerDependencies": {
49 | "react": ">=0.13.0"
50 | },
51 | "scripts": {
52 | "test": "mocha",
53 | "example": "webpack --config example/webpack.config.js",
54 | "compile": "babel src --out-dir dist",
55 | "prepublish": "npm run compile",
56 | "lint": "eslint ."
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/test/helpers/render.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Module dependencies
3 | */
4 |
5 | import expect from 'expect';
6 | import proxyquire from 'proxyquire';
7 | import React from 'react';
8 | import ReactDOM from 'react-dom';
9 | import TestUtils from 'react-addons-test-utils';
10 |
11 | /**
12 | * Stub out SoundCloud
13 | */
14 |
15 | function setup() {
16 | const widgetStub = {
17 | load: expect.createSpy(),
18 | bind: expect.createSpy(),
19 | unbind: expect.createSpy(),
20 | };
21 |
22 | const SoundCloud = proxyquire('../../src/SoundCloud', {
23 | './lib/createWidget': {
24 | default: (id, cb) => cb(widgetStub),
25 | },
26 | }).default;
27 |
28 | return {
29 | widgetStub,
30 | SoundCloud,
31 | };
32 | }
33 |
34 | /**
35 | * Shallow rendering
36 | */
37 |
38 | export function render(props) {
39 | const { SoundCloud } = setup();
40 |
41 | const renderer = TestUtils.createRenderer();
42 | renderer.render(React.createElement(SoundCloud, props));
43 |
44 | const output = renderer.getRenderOutput();
45 |
46 | function rerender(newProps = {}) {
47 | renderer.render(React.createElement(SoundCloud, {
48 | ...props,
49 | ...newProps,
50 | }));
51 |
52 | return renderer.getRenderOutput();
53 | }
54 |
55 | return {
56 | props,
57 | output,
58 | rerender,
59 | };
60 | }
61 |
62 | /**
63 | * Full rendering into the dom
64 | */
65 |
66 | export function renderDOM(props) {
67 | const { widgetStub, SoundCloud } = setup();
68 |
69 | /**
70 | * Emulate changes to component.props using a container component's state
71 | */
72 |
73 | class Container extends React.Component {
74 | constructor(_props) {
75 | super(_props);
76 |
77 | this.state = _props;
78 | }
79 |
80 | render() {
81 | return ;
82 | }
83 | }
84 |
85 | const div = document.createElement('div');
86 | const container = ReactDOM.render(, div);
87 | const output = TestUtils.findRenderedComponentWithType(container, SoundCloud);
88 |
89 | function rerender(newProps = {}) {
90 | container.setState(newProps);
91 | return output;
92 | }
93 |
94 | function unmount() {
95 | ReactDOM.unmountComponentAtNode(div);
96 | }
97 |
98 | return {
99 | props,
100 | output,
101 | rerender,
102 | unmount,
103 | widgetStub,
104 | };
105 | }
106 |
--------------------------------------------------------------------------------
/example/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import injectTapEventPlugin from 'react-tap-event-plugin';
4 | import CustomWidget from './components/CustomWidget';
5 | import OptionsTable from './components/OptionsTable';
6 | import OptionsInput from './components/OptionsInput';
7 |
8 | class Example extends React.Component {
9 | constructor(props) {
10 | super(props);
11 |
12 | this.state = {
13 | id: 'soundcloud-id',
14 | url: 'https://soundcloud.com/sylvanesso/coffee',
15 | opts: [
16 | {name: 'auto_play', purpose: 'Start playing the widget after it’s loaded', toggled: false},
17 | {name: 'visual', purpose: 'Display widget in visual mode', toggled: true},
18 | {name: 'buying', purpose: 'Show/hide buy buttons', toggled: true},
19 | {name: 'liking', purpose: 'Show/hide like buttons', toggled: true},
20 | {name: 'download', purpose: 'Show/hide download buttons', toggled: true},
21 | {name: 'sharing', purpose: 'Show/hide share buttons/dialogues', toggled: true},
22 | {name: 'show_artwork', purpose: 'Show/hide artwork', toggled: true},
23 | {name: 'show_comments', purpose: 'Show/hide comments', toggled: true},
24 | {name: 'show_playcount', purpose: 'Show/hide number of sound plays', toggled: true},
25 | {name: 'show_user', purpose: 'Show/hide the uploader name', toggled: true},
26 | {name: 'show_reposts', purpose: 'Show/hide reposts', toggled: false},
27 | {name: 'hide_related', purpose: 'Show/hide related tracks', toggled: false},
28 | ],
29 | };
30 | }
31 |
32 | render() {
33 | return (
34 |
35 |
36 |
41 |
42 |
43 |
44 | this.setState({ url })} />
48 | this.setState({ id })} />
52 | this.setState({ opts })} />
55 |
56 |
57 | );
58 | }
59 | }
60 |
61 | injectTapEventPlugin();
62 | ReactDOM.render(, document.getElementById('react-root'));
63 |
--------------------------------------------------------------------------------
/test/SoundCloud-test.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect';
2 | import React from 'react';
3 | import { render, renderDOM } from './helpers/render';
4 |
5 | window.SC = {
6 | Widget: {
7 | Events: {
8 | PLAY: 'play',
9 | PAUSE: 'pause',
10 | FINISH: 'finish',
11 | },
12 | },
13 | };
14 |
15 | const url = 'https://soundcloud.com/sylvanesso/coffee';
16 |
17 | describe('SoundCloud Widget', () => {
18 | it('should render an iframe', () => {
19 | const { output } = render({ url });
20 | expect(output).toEqual(
21 |
29 | );
30 | });
31 |
32 | it('should render an iframe with custom id', () => {
33 | const { output } = render({ url, id: 'custom-id' });
34 | expect(output).toEqual(
35 |
43 | );
44 | });
45 |
46 | it('should render an iframe with custom height', () => {
47 | const { output } = render({ url, height: '200' });
48 | expect(output).toEqual(
49 |
57 | );
58 | });
59 |
60 | it('should render an iframe with height of 450px in visual mode', () => {
61 | const { output } = render({ url, opts: {visual: true} });
62 | expect(output).toEqual(
63 |
71 | );
72 | });
73 |
74 | it('should load a url', () => {
75 | expect(renderDOM({ url }).widgetStub.load.calls[0].arguments[0]).toBe(url);
76 | });
77 |
78 | it('should load a new url', () => {
79 | const { widgetStub, rerender } = renderDOM({ url });
80 | rerender({url: 'https://soundcloud.com/sylvanesso/hskt'});
81 | expect(widgetStub.load.calls[1].arguments[0]).toBe('https://soundcloud.com/sylvanesso/hskt');
82 | });
83 |
84 | it('should only load new urls', () => {
85 | const { widgetStub, rerender } = renderDOM({ url });
86 | rerender({ url }); // this shouldn't do anything
87 | expect(widgetStub.load.calls.length).toBe(1);
88 | });
89 |
90 | it('should bind event handler props', () => {
91 | expect(renderDOM({ url }).widgetStub.bind.calls.length).toBe(3);
92 | });
93 |
94 | it('should unbind event handler props before unmounting', () => {
95 | const { widgetStub, unmount } = renderDOM({ url });
96 | unmount();
97 | expect(widgetStub.unbind.calls.length).toBe(3);
98 | });
99 | });
100 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-soundcloud-widget [](https://travis-ci.org/troybetz/react-soundcloud-widget)
2 |
3 | Simple [React](http://facebook.github.io/react) component acting as a thin
4 | layer over the [SoundCloud HTML5 Widget](https://developers.soundcloud.com/docs/api/html5-widget).
5 |
6 | ## Features
7 |
8 | - url playback
9 | - customizable widget options
10 | - playback event bindings
11 | - lazy API loading
12 |
13 | ## Installation
14 |
15 | ```shell
16 | $ npm install react-soundcloud-widget
17 | ```
18 |
19 | ## Usage
20 |
21 | ```js
22 | 'react-sc-widget'
25 | opts={object} // defaults -> './lib/default-options'
26 | onPlay={func} // defaults -> noop
27 | onPause={func} // defaults -> noop
28 | onEnd={func} // defaults -> noop
29 | />
30 | ```
31 |
32 | ## Example
33 |
34 | ```js
35 | class Example extends Component {
36 | onPlay() {
37 | console.log('playing');
38 | }
39 |
40 | render() {
41 | return (
42 |
46 | );
47 | }
48 | }
49 |
50 | ```
51 |
52 | ### Widget options
53 |
54 | Boolean toggles passed via `props.opts`
55 |
56 | | Parameter | Purpose | Default|
57 | | --------|-------------|------|
58 | | `auto_play` | Start playing the widget after it’s loaded | `true` |
59 | | `visual` | Display widget in [visual mode](https://soundcheck.soundcloud.com/music/our-new-visual-player/). | `false` |
60 | | `buying` | Show/hide buy buttons | `true` |
61 | | `liking` | Show/hide like buttons | `true` |
62 | | `download` | Show/hide download buttons | `true` |
63 | | `sharing` | Show/hide share buttons/dialogues | `true` |
64 | | `show_artwork` | Show/hide artwork | `true` |
65 | | `show_comments` | Show/hide comments | `true` |
66 | | `show_playcount` | Show/hide number of sound plays | `true` |
67 | | `show_user` | Show/hide the uploader name | `true` |
68 | | `show_reposts` | Show/hide reposts | `false` |
69 | | `hide_related` | Show/hide related tracks | `false` |
70 |
71 | ## Warning
72 |
73 | Changing `props.url` currently adds an entry to `window.history`, breaking the back button (or at least adding another click to it).
74 |
75 | You can see this in action at http://troybetz.com/react-soundcloud-widget/, change the url using the button and try navigating back.
76 |
77 | This is outside my control for now, the widget used internally is served up and managed by SoundCloud. Super bummer.
78 |
79 | ## Caveat
80 |
81 | Programmatic control of the widget as outlined in the [API docs](https://developers.soundcloud.com/docs/api/html5-widget) isn't included. Luckily, the API loads alongside the widget, so taking control is as easy as:
82 |
83 | ```js
84 | var widget = SC.Widget('react-sc-player');
85 | // do stuff
86 | ```
87 |
88 | The component itself uses `SC.Widget.load`, `SC.Widget.bind` and `SC.Widget.unbind` internally. Using those methods outside the component may cause problems.
89 |
90 | # License
91 |
92 | MIT
93 |
--------------------------------------------------------------------------------
/src/SoundCloud.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Module dependencies
3 | */
4 |
5 | import React from 'react';
6 | import createWidget from './lib/createWidget';
7 |
8 | /**
9 | * Create a new `SoundCloud` component.
10 | *
11 | * This is essentially a glorified wrapper over the existing
12 | * HTML5 widget from SoundCloud. Programmatic control not included.
13 | *
14 | * NOTE: Changing `props.url` will cause the component to load it.
15 | * Unfortunately, SoundCloud adds an entry to `window.history` every time
16 | * a new url is loaded, so changing `props.url` __will__ break the back button.
17 | */
18 |
19 | class SoundCloud extends React.Component {
20 |
21 | /**
22 | * @param {Object} props
23 | */
24 |
25 | constructor(props) {
26 | super(props);
27 | this._internalWidget = null;
28 | }
29 |
30 | componentDidMount() {
31 | this._createWidget();
32 | }
33 |
34 | /**
35 | * @param {Object} nextProps
36 | * @returns {Boolean}
37 | */
38 |
39 | shouldComponentUpdate(nextProps) {
40 | return nextProps.url !== this.props.url;
41 | }
42 |
43 | componentDidUpdate() {
44 | this._reloadWidget();
45 | }
46 |
47 | componentWillUnmount() {
48 | this._unbindEvents();
49 | }
50 |
51 | /**
52 | * Called on the initial render, this uses the rendered iframe
53 | * as a base for creating a new `_internalWidget`.
54 | */
55 |
56 | _createWidget() {
57 | createWidget(this.props.id, (widget) => {
58 | this._setupWidget(widget);
59 | this._reloadWidget();
60 | });
61 | }
62 |
63 | /**
64 | * Integrate a newly created `widget` with the rest of the component.
65 | *
66 | * @param {Object} Widget
67 | */
68 |
69 | _setupWidget(widget) {
70 | this._internalWidget = widget;
71 | this._bindEvents();
72 | }
73 |
74 | /**
75 | * This is the only way to manipulate the embedded iframe, it's essentially
76 | * refreshed and reloaded.
77 | *
78 | * NOTE: SoundCloud adds an entry to `window.history` after reloading. This is
79 | * __really__ annoying, but unavoidable at the moment, so basically every
80 | * time the url changes it breaks the back button. Super bummer.
81 | */
82 |
83 | _reloadWidget() {
84 | this._internalWidget.load(this.props.url, this.props.opts);
85 | }
86 |
87 | /**
88 | * Listen for events coming from `widget`, and pass them up the
89 | * chain to the parent component if needed.
90 | */
91 |
92 | _bindEvents() {
93 | this._internalWidget.bind(window.SC.Widget.Events.PLAY, this.props.onPlay);
94 | this._internalWidget.bind(window.SC.Widget.Events.PAUSE, this.props.onPause);
95 | this._internalWidget.bind(window.SC.Widget.Events.FINISH, this.props.onEnd);
96 | }
97 |
98 | /**
99 | * Remove all event bindings.
100 | */
101 |
102 | _unbindEvents() {
103 | this._internalWidget.unbind(window.SC.Widget.Events.PLAY);
104 | this._internalWidget.unbind(window.SC.Widget.Events.PAUSE);
105 | this._internalWidget.unbind(window.SC.Widget.Events.FINISH);
106 | }
107 |
108 | /**
109 | * @returns {Object}
110 | */
111 |
112 | render() {
113 | return (
114 |
121 | );
122 | }
123 | }
124 |
125 | SoundCloud.propTypes = {
126 | // url to play. It's kept in sync, changing it will
127 | // cause the widget to refresh and play the new url.
128 | url: React.PropTypes.string.isRequired,
129 |
130 | // custom ID for widget iframe element
131 | id: React.PropTypes.string,
132 |
133 | height: React.PropTypes.oneOfType([
134 | React.PropTypes.string,
135 | React.PropTypes.number,
136 | ]),
137 |
138 | // widget parameters for appearance and auto play.
139 | opts: React.PropTypes.objectOf(React.PropTypes.bool),
140 |
141 | // event subscriptions
142 | onPlay: React.PropTypes.func,
143 | onPause: React.PropTypes.func,
144 | onEnd: React.PropTypes.func,
145 | };
146 |
147 | SoundCloud.defaultProps = {
148 | id: 'react-sc-widget',
149 | opts: {},
150 | onPlay: () => {},
151 | onPause: () => {},
152 | onEnd: () => {},
153 | };
154 |
155 | /**
156 | * Expose `SoundCloud` component
157 | */
158 |
159 | export default SoundCloud;
160 |
--------------------------------------------------------------------------------