diff --git a/packages/ndla-audio-search/README.md b/packages/ndla-audio-search/README.md new file mode 100644 index 0000000000..89e7a1ee01 --- /dev/null +++ b/packages/ndla-audio-search/README.md @@ -0,0 +1,61 @@ +# ndla-audio-search + +A simple library for selecting NDLA audio files. + +## Installation + +```sh +$ npm install ndla-audio-search +``` + +## Usage + +### Get audio with the audio selector + +To use the `AudioSearch` component, some functions for handling search and fetching audios are needed. In addition, some translations are needed. +```js +import AudioSearch from 'ndla-audio-search'; + +const searchAudios = queryObject => { + // Return new Promise of audio objects +}; + +const fetchAudio = id => { + // Return new Promise of a single audio object +}; + +const onError = err => { + // Handle error +}; + +const audioSelect = audio => { + // Handle audio selection +}; + +const translations = { + searchPlaceholder: /* Translated string */, + searchButtonTitle: /* Translated string */, + useAudio: /* Translated string */, + noResults: /* Translated string */, +}; + + + +``` + +A `queryObject` must look like this: +```js +{ + query: /* Query string */, + page: /* Page number */, + pageSize: /* Page size (elements per page) */, + locale: /* The search language; usually provided by the front-end */, +} +``` diff --git a/packages/ndla-audio-search/package.json b/packages/ndla-audio-search/package.json new file mode 100644 index 0000000000..631b360a08 --- /dev/null +++ b/packages/ndla-audio-search/package.json @@ -0,0 +1,47 @@ +{ + "name": "ndla-audio-search", + "version": "0.1.1-0", + "description": "A simple library for searching for audio files from NDLA", + "license": "GPL-3.0", + "main": "lib/index.js", + "module": "es/index.js", + "jsnext:main": "es/index.js", + "scripts": { + "build": "npm run build:commonjs && npm run build:es", + "build:commonjs": "$(cd ..; npm bin)/cross-env BABEL_ENV=commonjs $(cd ..; npm bin)/babel src --out-dir lib --ignore __tests__", + "build:es": "$(cd ..; npm bin)/cross-env BABEL_ENV=es $(cd ..; npm bin)/babel src --out-dir es --ignore __tests__", + "clean": "$(cd ..; npm bin)/rimraf lib es", + "test": "$(cd ..; npm bin)/jest", + "prepublish": "npm run clean && npm run build" + }, + "repository": { + "type": "git", + "url": "https://github.com/NDLANO/frontend-packages.git/ndla-ui/" + }, + "keywords": [ + "ndla" + ], + "author": "ndla@knowit.no", + "files": [ + ], + "devDependencies": { + "ndla-article-scripts": "^0.0.3", + "ndla-licenses": "^0.0.7", + "ndla-ui": "^0.7.4", + "ndla-util": "^0.1.3", + "react-bem-helper": "^1.2.0" + }, + "peerDependencies": { + "classnames": "^2.2.5", + "ndla-article-scripts": "^0.0.3", + "ndla-licenses": "^0.0.7", + "ndla-ui": "^0.7.4", + "ndla-util": "^0.1.3", + "react": "^15.0.0", + "react-bem-helper": "^1.2.0", + "react-dom": "^15.0.0" + }, + "dependencies": { + "defined": "1.0.0" + } +} diff --git a/packages/ndla-audio-search/src/AudioBar.js b/packages/ndla-audio-search/src/AudioBar.js new file mode 100644 index 0000000000..77bc613e64 --- /dev/null +++ b/packages/ndla-audio-search/src/AudioBar.js @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2017-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +/* eslint jsx-a11y/media-has-caption: 0 */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import BEMHelper from 'react-bem-helper'; + +const classes = new BEMHelper({ + name: 'audio-bar', + prefix: 'c-', +}); + +class AudioBar extends Component { + constructor(props) { + super(props); + this.state = { + audioSource: undefined, + audioType: undefined, + }; + + this.loadAudio = this.loadAudio.bind(this); + } + + loadAudio() { + this.props + .fetchAudio(this.props.audio.id) + .then(result => { + this.setState({ + audioSource: result.audioFile.url, + audioType: result.audioFile.mimeType, + }); + }) + .catch(err => { + this.props.onError(err); + }); + } + + render() { + const { audioSource, audioType } = this.state; + return ( +
+ +
+ ); + } +} + +AudioBar.propTypes = { + audio: PropTypes.shape({ + id: PropTypes.number.isRequired, + }), + fetchAudio: PropTypes.func.isRequired, + onError: PropTypes.func.isRequired, +}; + +export default AudioBar; diff --git a/packages/ndla-audio-search/src/AudioSearch.js b/packages/ndla-audio-search/src/AudioSearch.js new file mode 100644 index 0000000000..59c96848a5 --- /dev/null +++ b/packages/ndla-audio-search/src/AudioSearch.js @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2017-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Pager } from 'ndla-ui'; +import BEMHelper from 'react-bem-helper'; + +import AudioSearchForm from './AudioSearchForm'; +import AudioSearchList from './AudioSearchList'; + +const classes = new BEMHelper({ + name: 'audio-search', + prefix: 'c-', +}); + +class AudioSearch extends Component { + constructor(props) { + super(props); + this.state = { + queryObject: props.queryObject, + audios: [], + lastPage: 0, + totalCount: 0, + searching: false, + }; + + this.submitAudioSearchQuery = this.submitAudioSearchQuery.bind(this); + this.searchAudios = this.searchAudios.bind(this); + } + + componentDidMount() { + this.searchAudios(this.state.queryObject); + } + + submitAudioSearchQuery(queryObject) { + this.searchAudios({ + query: queryObject.query, + page: 1, + pageSize: queryObject.pageSize, + locale: queryObject.locale, + }); + } + + searchAudios(queryObject) { + this.setState({ searching: true }); + this.props + .searchAudios(queryObject) + .then(result => { + this.setState({ + queryObject: { + query: queryObject.query, + page: queryObject.page, + pageSize: result.pageSize, + locale: queryObject.locale, + }, + audios: result.results, + totalCount: result.totalCount, + lastPage: Math.ceil(result.totalCount / result.pageSize), + searching: false, + }); + }) + .catch(err => { + this.props.onError(err); + this.setState({ searching: false }); + }); + } + + render() { + const { fetchAudio, onError, translations } = this.props; + const { queryObject, audios, lastPage, searching } = this.state; + const { page, locale } = queryObject; + + return ( +
+ + + +
+ ); + } +} + +AudioSearch.propTypes = { + queryObject: PropTypes.shape({ + query: PropTypes.string, + page: PropTypes.number.isRequired, + pageSize: PropTypes.number.isRequired, + locale: PropTypes.string.isRequired, + }), + fetchAudio: PropTypes.func.isRequired, + searchAudios: PropTypes.func.isRequired, + onError: PropTypes.func.isRequired, + translations: PropTypes.shape({ + searchPlaceholder: PropTypes.string.isRequired, + searchButtonTitle: PropTypes.string.isRequired, + useAudio: PropTypes.string.isRequired, + noResults: PropTypes.string.isRequired, + }), +}; + +export default AudioSearch; diff --git a/packages/ndla-audio-search/src/AudioSearchForm.js b/packages/ndla-audio-search/src/AudioSearchForm.js new file mode 100644 index 0000000000..75ad44f0b2 --- /dev/null +++ b/packages/ndla-audio-search/src/AudioSearchForm.js @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2017-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Button } from 'ndla-ui'; +import BEMHelper from 'react-bem-helper'; + +const classes = new BEMHelper({ + name: 'audio-search', + prefix: 'c-', +}); + +class AudioSearchForm extends Component { + constructor(props) { + super(props); + this.state = { + queryObject: props.queryObject, + }; + this.onKeyPress = this.onKeyPress.bind(this); + this.handleQueryChange = this.handleQueryChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + } + + onKeyPress(evt) { + if (evt.key === 'Enter') { + this.handleSubmit(evt); + } + } + + handleQueryChange(evt) { + this.setState({ + queryObject: { + query: evt.target.value, + page: this.state.queryObject.page, + pageSize: this.state.queryObject.pageSize, + locale: this.state.queryObject.locale, + }, + }); + } + + handleSubmit(evt) { + evt.preventDefault(); + this.props.onSearchQuerySubmit(this.state.queryObject); + } + + render() { + const { searching, translations } = this.props; + + return ( +
+ + +
+ ); + } +} + +AudioSearchForm.propTypes = { + queryObject: PropTypes.shape({ + query: PropTypes.string, + page: PropTypes.number.isRequired, + pageSize: PropTypes.number.isRequired, + locale: PropTypes.string.isRequired, + }), + translations: PropTypes.shape({ + searchPlaceholder: PropTypes.string.isRequired, + searchButtonTitle: PropTypes.string.isRequired, + }), + searching: PropTypes.bool.isRequired, + onSearchQuerySubmit: PropTypes.func.isRequired, +}; + +export default AudioSearchForm; diff --git a/packages/ndla-audio-search/src/AudioSearchList.js b/packages/ndla-audio-search/src/AudioSearchList.js new file mode 100644 index 0000000000..144494be73 --- /dev/null +++ b/packages/ndla-audio-search/src/AudioSearchList.js @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2017-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import BEMHelper from 'react-bem-helper'; +import AudioSearchResult from './AudioSearchResult'; + +const classes = new BEMHelper({ + name: 'audio-search-list', + prefix: 'c-', +}); + +export default function AudioSearchList({ + audios, + searching, + locale, + translations, + onError, + fetchAudio, +}) { + if ((!audios || audios.length === 0) && !searching) { + return ( +

+ {translations.noResults} +

+ ); + } + if (searching && !(audios.length > 0)) { + return
; + } + return ( +
+ {audios.map(audio => + , + )} +
+ ); +} + +AudioSearchList.propTypes = { + audios: PropTypes.array.isRequired, + searching: PropTypes.bool.isRequired, + locale: PropTypes.string.isRequired, + translations: PropTypes.shape({ + noResults: PropTypes.string.isRequired, + useAudio: PropTypes.string.isRequired, + }), + onError: PropTypes.func.isRequired, + fetchAudio: PropTypes.func.isRequired, +}; diff --git a/packages/ndla-audio-search/src/AudioSearchResult.js b/packages/ndla-audio-search/src/AudioSearchResult.js new file mode 100644 index 0000000000..27fd2006a7 --- /dev/null +++ b/packages/ndla-audio-search/src/AudioSearchResult.js @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2017-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import BEMHelper from 'react-bem-helper'; +import { getLicenseByAbbreviation } from 'ndla-licenses'; +import { Button, LicenseIconList } from 'ndla-ui'; +import AudioBar from './AudioBar'; + +const classes = new BEMHelper({ + name: 'audio-search', + prefix: 'c-', +}); + +export default function AudioSearchResult({ + audio, + fetchAudio, + onError, + locale, + translations, +}) { + const license = getLicenseByAbbreviation(audio.license, locale); + return ( +
+
+

+ {audio.title} +

+
+ {license.rights + ? + : license} +
+ +
+ +
+ ); +} + +AudioSearchResult.propTypes = { + audio: PropTypes.shape({ + id: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + }), + translations: PropTypes.shape({ + useAudio: PropTypes.string.isRequired, + }), + fetchAudio: PropTypes.func.isRequired, + onError: PropTypes.func.isRequired, + locale: PropTypes.string.isRequired, +}; diff --git a/packages/ndla-audio-search/src/index.js b/packages/ndla-audio-search/src/index.js new file mode 100644 index 0000000000..8e7ff4b911 --- /dev/null +++ b/packages/ndla-audio-search/src/index.js @@ -0,0 +1,3 @@ +import AudioSearch from './AudioSearch'; + +export default AudioSearch; diff --git a/packages/ndla-audio-search/yarn.lock b/packages/ndla-audio-search/yarn.lock new file mode 100644 index 0000000000..31e0cc838e --- /dev/null +++ b/packages/ndla-audio-search/yarn.lock @@ -0,0 +1,61 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +classnames@2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d" + +defined@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" + +js-tokens@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +loose-envify@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" + dependencies: + js-tokens "^3.0.0" + +ndla-article-scripts@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/ndla-article-scripts/-/ndla-article-scripts-0.0.3.tgz#e12827f66a0af970215f373736cf351ff312d0a2" + +ndla-licenses@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ndla-licenses/-/ndla-licenses-0.0.7.tgz#8337dc92c5726706c936c2b2691130037ee43f6f" + dependencies: + defined "1.0.0" + +ndla-ui@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/ndla-ui/-/ndla-ui-0.4.3.tgz#6ffbe64fc34800751b93d28a51e271c2ec509b19" + dependencies: + classnames "2.2.5" + react-bem-helper "1.4.1" + react-prop-types "0.4.0" + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +react-bem-helper@1.4.1, react-bem-helper@^1.2.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/react-bem-helper/-/react-bem-helper-1.4.1.tgz#cb27aeadda020e9fe9d169bfaae1095c559f4cf5" + dependencies: + object-assign "^4.1.1" + +react-prop-types@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/react-prop-types/-/react-prop-types-0.4.0.tgz#f99b0bfb4006929c9af2051e7c1414a5c75b93d0" + dependencies: + warning "^3.0.0" + +warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" + dependencies: + loose-envify "^1.0.0" diff --git a/packages/ndla-ui/.storybook/config.js b/packages/ndla-ui/.storybook/config.js index 5c1a11d980..53e6c69fc1 100644 --- a/packages/ndla-ui/.storybook/config.js +++ b/packages/ndla-ui/.storybook/config.js @@ -2,6 +2,7 @@ import { configure } from '@storybook/react'; import { setOptions } from '@storybook/addon-options'; import '../src/main.scss'; import '../src/editor.scss'; +import '../src/audioSearch.scss'; import '../src/imageSearch.scss'; import '../src/videoSearch.scss'; diff --git a/packages/ndla-ui/package.json b/packages/ndla-ui/package.json index c166fba30c..42152a8d89 100644 --- a/packages/ndla-ui/package.json +++ b/packages/ndla-ui/package.json @@ -60,6 +60,7 @@ "isomorphic-fetch": "^2.2.1", "moment": "^2.17.1", "ndla-article-scripts": "^0.0.7", + "ndla-audio-search": "^0.1.1-0", "ndla-editor": "^0.1.6", "ndla-image-search": "^0.1.6", "ndla-licenses": "^0.0.9", diff --git a/packages/ndla-ui/src/audioSearch.scss b/packages/ndla-ui/src/audioSearch.scss new file mode 100644 index 0000000000..5fa46926d7 --- /dev/null +++ b/packages/ndla-ui/src/audioSearch.scss @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2017-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +@import "global/settings/settings.config"; +@import "global/settings/settings.colors"; + +.c-audio-search { + padding: 1rem; + border: 1px solid $brand-color--lighter; + border-radius: 0.2rem; + + &__form-query { + width: 88%; + height: 3rem; + margin-top: 0; + margin-bottom: 0; + box-shadow: none; + display: inline-block; + border-width: 2px; + float: left; + border-radius: 5px 0 0 5px; + } + + &__border { + margin: 1em -2em; + height: 1px; + border-bottom: 1px solid $brand-color--lighter; + } + + &__form { + width: 100%; + padding-left: 0; + padding-right: 0; + margin-bottom: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid #eaeaea; + } + + &__license { + margin-bottom: 25px; + } + &__form-button { + width: 12%; + height: 3rem; + display: inline-block; + margin-left: -2px; + border-radius: 0 5px 5px 0; + padding: 0.4em; + + &:hover { + transform: none; + } + } + + &__list { + &-item { + &:not(:last-child) { + padding-bottom: 1em; + border-bottom: 1px solid $brand-color--lighter; + } + + &-inner { + .c-license-icons { + &__list { + display: flex; + flex-flow: row wrap; + align-content: flex-start; + min-height: 24px; + margin-bottom: 5px; + } + + &__item { + max-height: 0; + } + } + + .c-audio-bar { + margin-bottom: 10px; + + // The 'audio' tag is implemented with a download button in Chrome > 58. + // Make it so that this button doesn't show. + audio::-webkit-media-controls { + overflow: hidden !important + } + audio::-webkit-media-controls-enclosure { + width: calc(100% + 32px); + margin-left: auto; + } + } + + h2 { + margin-top: 30px; + } + } + } + } + + &-list { + &__result-spinner { + border: 0.4em solid $brand-grey--light; + border-bottom-color: $brand-color; + border-radius: 50%; + margin: 0 auto; + animation: loadVideoSpinner 0.7s linear infinite; + height: 3em; + width: 3em; + } + } +} + +@keyframes loadVideoSpinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/packages/ndla-ui/stories/collated-components.jsx b/packages/ndla-ui/stories/collated-components.jsx index 53eb948a87..d3c6ff9242 100644 --- a/packages/ndla-ui/stories/collated-components.jsx +++ b/packages/ndla-ui/stories/collated-components.jsx @@ -39,6 +39,7 @@ import { } from './molecules/resources'; import LicenseExample, { LicenseBox } from './article/LicenseExample'; import ImageSearcher from './molecules/imageSearch'; +import AudioSearcher from './molecules/audioSearch'; import VideoSearcher from './molecules/videoSearch'; const toggle = () => { @@ -139,6 +140,18 @@ storiesOf('Sammensatte moduler', module) , ) + .add('Bildesøk', () => +
+

Bildesøk

+
+

+ Bildesøk som gjør det mulig å søke mot NDLA sitt bilde api. Denne + modulen krever at det både finnes en token og api url. +

+ +
+
, + ) .add('Emne artikkel', () =>
@@ -177,18 +190,6 @@ storiesOf('Sammensatte moduler', module)
, ) - .add('Bildesøk', () => -
-

Bildesøk

-
-

- Bildesøk som gjør det mulig å søke mot NDLA sitt bilde api. Denne - modulen krever at det både finnes en token og api url. -

- -
-
, - ) .add('Filter', () =>
@@ -284,6 +285,18 @@ storiesOf('Sammensatte moduler', module)
, ) + .add('Lydsøk', () => +
+

Lydsøk

+
+

+ Lydsøk som gjør det mulig å søke mot NDLA sitt lyd-api. Denne modulen + krever at det både finnes en token og api url. +

+ +
+
, + ) .add('Læringsressurser enkeltstående', () =>
diff --git a/packages/ndla-ui/stories/molecules/audioSearch.jsx b/packages/ndla-ui/stories/molecules/audioSearch.jsx new file mode 100644 index 0000000000..578ce72d73 --- /dev/null +++ b/packages/ndla-ui/stories/molecules/audioSearch.jsx @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2017-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import React from 'react'; +import AudioSearch from 'ndla-audio-search'; +import { headerWithAccessToken, getToken } from '../apiFunctions'; + +const fetchAudios = queryObject => { + const { query, page, pageSize, locale } = queryObject; + const queryString = `${query + ? `query=${query}&` + : ''}page=${page}&page-size=${pageSize}&language=${locale}`; + return new Promise((resolve, reject) => { + getToken().then(token => { + fetch(`https://test.api.ndla.no/audio-api/v1/audio/?${queryString}`, { + method: 'GET', + headers: headerWithAccessToken(token), + }).then(res => { + if (res.ok) return resolve(res.json()); + return res.json().then(json => reject(json)); + }); + }); + }); +}; + +const fetchAudio = id => + new Promise((resolve, reject) => { + getToken().then(token => { + fetch(`https://test.api.ndla.no/audio-api/v1/audio/${id}`, { + method: 'GET', + headers: headerWithAccessToken(token), + }).then(res => { + if (res.ok) return resolve(res.json()); + return res.json().then(json => reject(json)); + }); + }); + }); + +export const AudioSearcher = () => { + const audioSelect = audio => { + console.log(audio); + }; + + const onError = err => { + console.error(err); + }; + + const defaultQueryObject = { + query: '', + page: 1, + pageSize: 16, + locale: 'nb', + }; + + const translations = { + searchPlaceholder: 'Søk i lydfiler', + searchButtonTitle: 'Søk', + useAudio: 'Velg lyd', + noResults: 'Ingen resultater funnet', + }; + + return ( + + ); +}; + +export default AudioSearcher; diff --git a/packages/ndla-ui/webpack.config.js b/packages/ndla-ui/webpack.config.js index 8d04bec66d..102229c657 100644 --- a/packages/ndla-ui/webpack.config.js +++ b/packages/ndla-ui/webpack.config.js @@ -5,6 +5,7 @@ module.exports = { entry: { main: path.resolve(__dirname, 'src/main.scss'), editor: path.resolve(__dirname, 'src/editor.scss'), + audioSearch: path.resolve(__dirname, 'src/audioSearch.scss'), imageSearch: path.resolve(__dirname, 'src/imageSearch.scss'), videoSearch: path.resolve(__dirname, 'src/videoSearch.scss'), },