diff --git a/.editorconfig b/.editorconfig index 2536d66bf..5760be583 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,7 +3,7 @@ root = true [*] indent_style = space -indent_size = 4 +indent_size = 2 charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true diff --git a/.env b/.env index f21f36089..610c409b2 100644 --- a/.env +++ b/.env @@ -1,6 +1,6 @@ NODE_ENV=development PORT=8000 -API_URL=http://api.quran.com:3000 +API_URL=http://quran.com:3000 SEGMENTS_KEY= SENTRY_KEY_CLIENT= SENTRY_KEY_SERVER= diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..1ee2668e7 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +src/components/SurahInfo/htmls/* diff --git a/.eslintrc b/.eslintrc index 839cbb37b..9df98661b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,40 +1,54 @@ { "parser": "babel-eslint", + "extends": "airbnb", "env": { - "es6": true, - "node": true, "browser": true, - "jquery": true - }, - "ecmaFeatures": { - "arrowFunctions": true, - "binaryLiterals": true, - "blockBindings": true, - "classes": true, - "defaultParams": true, - "destructuring": true, - "forOf": true, - "generators": true, - "modules": true, - "objectLiteralComputedProperties": true, - "objectLiteralDuplicateProperties": true, - "objectLiteralShorthandMethods": true, - "objectLiteralShorthandProperties": true, - "octalLiterals": true, - "regexUFlag": true, - "regexYFlag": true, - "spread": true, - "superInFunctions": true, - "templateStrings": true, - "unicodeCodePointEscapes": true, - "globalReturn": true, - "jsx": false + "node": true, + "mocha": true, + "es6": true }, "rules": { - "strict": 0, - "indent": [2, 2], - "quotes": [2, "single"], - "no-unused-vars": 0 + "react/no-multi-comp": 0, + "import/default": 0, + "import/no-duplicates": 0, + "import/named": 0, + "import/namespace": 0, + "import/no-unresolved": 0, + "import/no-named-as-default": 2, + // Temporarirly disabled due to a possible bug in babel-eslint (todomvc example) + "block-scoped-var": 0, + // Temporarily disabled for test/* until babel/babel-eslint#33 is resolved + "padded-blocks": 0, + "comma-dangle": 0, // not sure why airbnb turned this on. gross! + "indent": [2, 2, {"SwitchCase": 1}], + "no-console": 0, + "no-alert": 0, + "object-curly-spacing": 0, + "no-case-declarations": 0 + }, + "plugins": [ + "react", "import" + ], + "settings": { + "import/parser": "babel-eslint", + "import/resolve": { + moduleDirectory: ["node_modules", "src"] + } + }, + "parserOptions":{ + "ecmaFeatures": { + "experimentalObjectRestSpread": true + } }, - "plugins": ["react"] + "globals": { + "__DEVELOPMENT__": true, + "__CLIENT__": true, + "__SERVER__": true, + "__DISABLE_SSR__": true, + "__DEVTOOLS__": true, + "socket": true, + "webpackIsomorphicTools": true, + ga: true, + Raven: true + } } diff --git a/client.js b/client.js index 691b641ed..b4491dd80 100644 --- a/client.js +++ b/client.js @@ -1,4 +1,4 @@ -/*global document, window, $ */ +/* global document, window, $ */ import 'babel-polyfill'; import React from 'react'; @@ -21,15 +21,19 @@ import createStore from './src/redux/create'; import routes from './src/routes'; const client = new ApiClient(); -const store = createStore(browserHistory, client, window.__data); +const store = createStore(browserHistory, client, window.reduxData); const history = syncHistoryWithStore(browserHistory, store); -Raven.config(config.sentryClient).install() +try { + Raven.config(config.sentryClient).install(); +} catch (error) { + console.log(error); +} window.quranDebug = debug; window.ReactDOM = ReactDOM; // For chrome dev tool support -window.clearCookies = function() { +window.clearCookies = () => { reactCookie.remove('quran'); reactCookie.remove('content'); reactCookie.remove('audio'); @@ -38,7 +42,7 @@ window.clearCookies = function() { // Init tooltip if (typeof window !== 'undefined') { - $(function () { + $(() => { $(document.body).tooltip({ selector: '[data-toggle="tooltip"]', animation: false @@ -69,6 +73,7 @@ match({ history, routes: routes() }, (error, redirectLocation, renderProps) => { {component} , mountNode, () => { - debug('client', 'React Rendered'); - }); + debug('client', 'React Rendered'); + } + ); }); diff --git a/package.json b/package.json index 601b12f2e..d88d26674 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "quran", - "version": "0.0.0", + "version": "1.0.0", "private": true, "scripts": { "test": "./node_modules/karma/bin/karma start", - "test:ci:unit": "./node_modules/karma/bin/karma start --browsers PhantomJS --single-run", + "test:ci:unit": "./node_modules/karma/bin/karma start --browsers PhantomJS --single-run; npm run test:ci:lint", "test:ci:functional": "node ./nightwatch.js -c ./nightwatch.json -e production", - "test:ci:lint": "eslint ./src/scripts/**/*.js", + "test:ci:lint": "eslint ./src/**/*.js", "test:dev:unit": "./node_modules/karma/bin/karma start", "test:dev:functional": "node ./nightwatch.js -c ./nightwatch.json", "test:dev:lint": "eslint ./src/scripts/**/*.js", @@ -59,6 +59,7 @@ "express-useragent": "^0.2.0", "extract-text-webpack-plugin": "^1.0.1", "file-loader": "^0.8.4", + "fontfaceobserver": "^1.7.1", "html-webpack-plugin": "^1.4.0", "http-proxy": "^1.13.2", "humps": "^1.0.0", @@ -92,6 +93,7 @@ "react-scroll": "^1.0.4", "redux": "^3.3.1", "redux-connect": "^2.4.0", + "reselect": "^2.5.1", "resolve-url": "^0.2.1", "sass-loader": "2.0.1", "scroll-behavior": "^0.3.3", @@ -117,9 +119,12 @@ "chromedriver": "^2.19.0", "del": "^2.0.2", "enzyme": "^2.2.0", - "eslint": "^1.4.1", - "eslint-loader": "^1.0.0", - "eslint-plugin-react": "^3.3.2", + "eslint": "^2.13.0", + "eslint-config-airbnb": "^9.0.1", + "eslint-loader": "^1.3.0", + "eslint-plugin-import": "^1.8.1", + "eslint-plugin-jsx-a11y": "^1.5.3", + "eslint-plugin-react": "^5.2.2", "jscs": "^2.1.1", "karma": "^0.13.9", "karma-chai": "^0.1.0", diff --git a/server.js b/server.js index 3b1ae7f13..55413f0f8 100644 --- a/server.js +++ b/server.js @@ -101,11 +101,9 @@ const port = process.env.PORT || 8000; export default function serve(cb) { return server.listen(port, function() { - console.info(` - ==> 🌎 ENV=${process.env.NODE_ENV} - ==> ✅ Server is listening at http://localhost:${port} - ==> 🎯 API at ${process.env.API_URL} - `); + console.info(`==> 🌎 ENV=${process.env.NODE_ENV}`); + console.info(`==> ✅ Server is listening at http://localhost:${port}`); + console.info(`==> 🎯 API at ${process.env.API_URL}`); Object.keys(config).forEach(key => config[key].constructor.name !== 'Object' && console.info(`==> ${key}`, config[key])); cb && cb(this); diff --git a/src/components/Audioplayer/Segments/index.js b/src/components/Audioplayer/Segments/index.js index 269f42532..c8035046e 100644 --- a/src/components/Audioplayer/Segments/index.js +++ b/src/components/Audioplayer/Segments/index.js @@ -1,5 +1,4 @@ import React, { Component, PropTypes } from 'react'; -import ReactDOM from 'react-dom'; import { decrypt } from 'sjcl'; export default class Segments extends Component { @@ -15,33 +14,26 @@ export default class Segments extends Component { currentWord: null }; - state = { - intervals: [], - }; - constructor() { - super(...arguments); + super(...arguments); // eslint-disable-line prefer-rest-params this.secret = process.env.SEGMENTS_KEY; this.currentWord = null; } + state = { + intervals: [], + }; + // LIFECYCLE METHODS componentDidMount() { - const builtIntervals = this.buildIntervals(); - //console.debug('Segments componentDidMount', this.props.audio, builtIntervals); this.bindListeners(); } - componentWillUnmount() { - //console.log('Segments componentWillUnmount', this.props.audio, { props: this.props, state: this.state }); - this.unbindListeners(); - } - componentWillReceiveProps(nextProps) { const prevProps = this.props; - if (prevProps.audio != nextProps.audio) { + if (prevProps.audio !== nextProps.audio) { this.unbindListeners(prevProps); this.buildIntervals(nextProps); @@ -52,26 +44,30 @@ export default class Segments extends Component { shouldComponentUpdate(nextProps, nextState) { const prevProps = this.props; const prevState = this.state; + return [ - prevProps.audio != nextProps.audio, - prevProps.segments != nextProps.segments, - prevState.intervals != nextState.intervals, - prevProps.currentWord != nextProps.currentWord, - nextProps.currentWord != this.currentWord + prevProps.audio !== nextProps.audio, + prevProps.segments !== nextProps.segments, + prevState.intervals !== nextState.intervals, + prevProps.currentWord !== nextProps.currentWord, + nextProps.currentWord !== this.currentWord ].some(b => b); - //return false; // TODO: I think we can just 'return false' here since there is nothing to actually render... // oh wait, maybe i need it so that componentDidUpdate will run..., despite render() not // actually being needed... dunno right now } - componentDidUpdate(prevProps, prevState) { - if (this.currentWord != this.props.currentWord) { // currentWord was changed by the user - if (this.props.currentWord != null) { + componentDidUpdate() { + if (this.currentWord !== this.props.currentWord) { // currentWord was changed by the user + if (this.props.currentWord !== null) { const wordInterval = this.state.words[this.props.currentWord.split(/:/).pop()]; - const timeToSeek = wordInterval.startTime + 0.001; // seek to the currentWord starting time and return + // seek to the currentWord starting time and return + const timeToSeek = wordInterval.startTime + 0.001; const isSeekable = this.props.audio.seekable && this.props.audio.seekable.length > 0; - const withinRange = !isSeekable? null : timeToSeek >= this.props.audio.seekable.start(0) && timeToSeek <= this.props.audio.seekable.end(0); + const withinRange = !isSeekable ? + null : + (timeToSeek >= this.props.audio.seekable.start(0) && + timeToSeek <= this.props.audio.seekable.end(0)); if (isSeekable && withinRange) { // seek to it this.props.audio.currentTime = timeToSeek; @@ -83,15 +79,24 @@ export default class Segments extends Component { this.props.audio.addEventListener('canplay', seekToTime); } } - return this.setCurrentWord(this.props.currentWord, 'componentDidUpdate'); // but don't forget to set the change internally for next time + + // but don't forget to set the change internally for next time + return this.setCurrentWord(this.props.currentWord, 'componentDidUpdate'); } + + return false; } - render() { - return (); + componentWillUnmount() { + this.unbindListeners(); } - // END LIFECYCLE METHODS + setCurrentWord(currentWord = null) { + // this is more immediately available but should eventually agree with props + this.currentWord = currentWord; + // calls the redux dispatch function passed down from the Audioplayer + this.props.onSetCurrentWord(currentWord); + } buildIntervals(props = this.props) { let segments = null; @@ -102,28 +107,32 @@ export default class Segments extends Component { } const words = {}; - const intervals = segments.map((segment, segmentIndex) => { - const startTime = segment[0], - endTime = segment[0] + segment[1], - duration = segment[1], - wordIndex = segment[2], - mappedVal = { startTime: startTime/1000, endTime: endTime/1000, duration: duration/1000 }; - - if (wordIndex >= 0 && !words[wordIndex]) + const intervals = segments.map(segment => { + const startTime = segment[0]; + const endTime = segment[0] + segment[1]; + const duration = segment[1]; + const wordIndex = segment[2]; + const mappedVal = { + startTime: startTime / 1000, + endTime: endTime / 1000, + duration: duration / 1000 + }; + + if (wordIndex >= 0 && !words[wordIndex]) { words[wordIndex] = mappedVal; + } - return [startTime/1000, endTime/1000, wordIndex]; + return [startTime / 1000, endTime / 1000, wordIndex]; }); - this.state.intervals = intervals; - this.state.words = words; + this.setState({ intervals, words }); + return { intervals, words }; // for console debugging } - bindListeners(props = this.props, state = this.state) { - const audio = props.audio; - const intervals = state.intervals; - const words = state.words; + bindListeners() { + const { audio, currentAyah, currentWord } = this.props; + const { intervals } = this.state; // Play listener const play = () => { @@ -131,8 +140,6 @@ export default class Segments extends Component { let repeaterId = null; new Promise((done, fail) => { - //console.debug('Play listener for '+ props.currentAyah +' started...'); - const intervalFn = () => { if (audio.seeking) return console.warn('we are seeking right now?'); if (audio.paused || audio.ended) return console.warn('stopped by running?'); @@ -141,13 +148,15 @@ export default class Segments extends Component { // the number of times we need to resort to a search, just in case logarithmic // time isn't good enough const index = this.binarySearch(intervals, audio.currentTime, this.compareFn); - const currentWord = index >= 0 && intervals[index][2] >= 0 ? - this.props.currentAyah +':'+ intervals[index][2] : null; + const word = index >= 0 && intervals[index][2] >= 0 ? + `${currentAyah}:${intervals[index][2]}` : + null; - if (currentWord == this.props.currentWord) return; // no work to be done - else if (currentWord == this.currentWord) return; // still no work to be done - else return this.setCurrentWord(currentWord, 'Play listener Do Stuff block'); // something changed, so we deal with it - } + if (word === currentWord) return false; // no work to be done + else if (word === this.currentWord) return false; // still no work to be done + // something changed, so we deal with it + return this.setCurrentWord(word, 'Play listener Do Stuff block'); + }; intervalFn(); repeaterId = setInterval(intervalFn, 30); @@ -161,14 +170,12 @@ export default class Segments extends Component { listeners[evName] = fail; audio.addEventListener(evName, listeners[evName], false); }); - }).then((ev) => { + }).then(() => { clearInterval(repeaterId); ['pause', 'ended', 'error', 'emptied', 'abort'].forEach((evName) => { audio.removeEventListener(evName, listeners[evName]); }); - - //console.debug('Play listener for '+ props.currentAyah +(ev && ev.type ? ' resolved by '+ ev.type : 'stopped') +' event'); }); }; audio.addEventListener('play', play, false); @@ -176,34 +183,37 @@ export default class Segments extends Component { this.setState({ listeners: { play }}); } - unbindListeners(props = this.props) { - props.audio.removeEventListener('play', this.state.listeners.play); - } + unbindListeners() { + const { audio } = this.props; - setCurrentWord(currentWord = null, debug = null) { - this.currentWord = currentWord; // this is more immediately available but should eventually agree with props - this.props.onSetCurrentWord(currentWord); // calls the redux dispatch function passed down from the Audioplayer - //console.log('setCurrentWord', currentWord, debug ? debug : ''); + audio.removeEventListener('play', this.state.listeners.play); } compareFn(time, interval) { if (time < interval[0]) return -1; else if (time > interval[1]) return 1; - else if (time == interval[0]) return 0; // floor inclusive - else if (time == interval[1]) return 1; - else return 0; + else if (time === interval[0]) return 0; // floor inclusive + else if (time === interval[1]) return 1; + + return 0; } binarySearch(ar, el, compareFn = (a, b) => (a - b)) { - var m = 0; - var n = ar.length - 1; + let m = 0; + let n = ar.length - 1; while (m <= n) { - var k = (n + m) >> 1; - var cmp = compareFn(el, ar[k]); - if (cmp > 0) m = k + 1; + const k = (n + m) >> 1; + const cmp = compareFn(el, ar[k]); + if (cmp > 0) m = k + 1; else if (cmp < 0) n = k - 1; else return k; } return -m - 1; } + + render() { + return ( + + ); + } } diff --git a/src/components/Audioplayer/Track/Tracker/index.js b/src/components/Audioplayer/Track/Tracker/index.js index 55853b7ec..919abbbe3 100644 --- a/src/components/Audioplayer/Track/Tracker/index.js +++ b/src/components/Audioplayer/Track/Tracker/index.js @@ -14,7 +14,7 @@ export default class Tracker extends Component { )}px`; element.parentElement.style.background = ( - `linear-gradient(to right, #2CA4AB 0%,#2CA4AB ${nextProps.progress}%,#635e49 ${nextProps.progress}%,#635e49 100%)` + `linear-gradient(to right, #2CA4AB 0%,#2CA4AB ${nextProps.progress}%,#635e49 ${nextProps.progress}%,#635e49 100%)` // eslint-disable-line max-len ); } diff --git a/src/components/Audioplayer/Track/index.js b/src/components/Audioplayer/Track/index.js index b9e3bd713..0c95f37d6 100644 --- a/src/components/Audioplayer/Track/index.js +++ b/src/components/Audioplayer/Track/index.js @@ -23,21 +23,12 @@ export default class Track extends Component { }; componentDidMount() { - this.setState({ mounted: true }); + this.setState({ mounted: true }); // eslint-disable-line react/no-did-mount-set-state if (this.props.file && __CLIENT__) { - //console.debug('Track componentDidMount', this.props.file, { file: this.props.file }); this.onFileLoad(this.props.file); } } - componentWillUnmount() { - //console.log('Track componentWillUnmount', this.props.file); - this.setState({ mounted: false }); // TODO, yeah, this is bad but we unmount and mount when - this.state.mounted = false; // we lazy load, and it causes problems... - // trace memory profile count - this.onFileUnload(this.props.file); - } - shouldComponentUpdate(nextProps, nextState) { return [ this.props.file.src !== nextProps.file.src, @@ -48,38 +39,38 @@ export default class Track extends Component { ].some(test => test); } - componentDidUpdate(prevProps, prevState) { + componentDidUpdate(prevProps) { if (this.props.file.src !== prevProps.file.src) { if (!prevProps.file.paused) { prevProps.file.pause(); } - prevProps.file.currentTime = 0; + prevProps.file.currentTime = 0; // eslint-disable-line no-param-reassign this.onFileUnload(prevProps.file); - this.setState({ progress: 0 }); + this.setState({ progress: 0 }); // eslint-disable-line react/no-did-update-set-state this.onFileLoad(this.props.file); } } + componentWillUnmount() { + this.setState({ mounted: false }); // TODO, yeah, this is bad but we unmount and mount when + this.state.mounted = false; // we lazy load, and it causes problems... + // trace memory profile count + this.onFileUnload(this.props.file); + } + onFileLoad(file) { // Preload file file.setAttribute('preload', 'auto'); - /* - if (!this.didAlreadyLoad) this.didAlreadyLoad = {}; - if (this.didAlreadyLoad[file.src]) return; - else this.didAlreadyLoad[file.src] = true; - */ - const loadeddata = () => { // Default current time to zero. This will change file.currentTime = 0; // eslint-disable-line no-param-reassign }; - file.onloadeddata = loadeddata; + file.onloadeddata = loadeddata; // eslint-disable-line no-param-reassign const timeupdate = () => { - //console.assert(this.state.mounted, 'timeupdate without being mounted', file, { file, mounted: this.state.mounted }); if (!this.state.mounted) return; // TODO needed? const progress = ( @@ -89,14 +80,14 @@ export default class Track extends Component { this.setState({ progress }); }; - file.ontimeupdate = timeupdate; + + file.ontimeupdate = timeupdate; // eslint-disable-line no-param-reassign const ended = () => { const { shouldRepeat, onEnd, isStarted, doStop, currentAyah, surah } = this.props; // if we're on the last ayah, do a full stop at the playback end - if (currentAyah == surah.id +':'+ surah.ayat) - return doStop(); + if (currentAyah === `${surah.id}:${surah.ayat}`) return doStop(); if (isStarted && shouldRepeat) { file.pause(); @@ -105,8 +96,11 @@ export default class Track extends Component { } else { onEnd(); } + + return false; }; - file.onended = ended; + + file.onended = ended; // eslint-disable-line no-param-reassign const play = () => { if (!this.state.mounted) return; @@ -116,17 +110,19 @@ export default class Track extends Component { this.setState({ currentTime }); }; - file.onplay = play; + + file.onplay = play; // eslint-disable-line no-param-reassign + + return false; } onFileUnload(file) { - //console.warn('onFileUnload'); if (!file.paused) { file.pause(); } } - onTrackerMove(event) { + onTrackerMove = (event) => { const { file } = this.props; const fraction = ( @@ -145,13 +141,20 @@ export default class Track extends Component { } render() { - const { progress, mounted } = this.state; + const { progress } = this.state; const { isStarted, file } = this.props; if (file.readyState >= 3) { - // the Math.round bit prevents us from trying to play again when we're effectively at the end of the audio file; this should allow shouldRepeat to work without getting overridden: - // ...but at the time I monkey-patched it, so we might be able to get rid of it since we cleaned up? Let's not for now... - if (isStarted && file.paused && file.readyState >= 3 && Math.round(file.currentTime) != Math.round(file.duration)) { + // the Math.round bit prevents us from trying to play again when we're + // effectively at the end of the audio file; this should allow + // shouldRepeat to work without getting overridden: + // ...but at the time I monkey-patched it, so we might be able to get + // rid of it since we cleaned up? Let's not for now... + if ( + isStarted && + file.paused && + file.readyState >= 3 && + Math.round(file.currentTime) !== Math.round(file.duration)) { file.play(); } else if (!isStarted && !file.paused) { file.pause(); @@ -159,8 +162,8 @@ export default class Track extends Component { } return ( -
- +
+
); } diff --git a/src/components/Audioplayer/index.js b/src/components/Audioplayer/index.js index d9252bfb9..7553db722 100644 --- a/src/components/Audioplayer/index.js +++ b/src/components/Audioplayer/index.js @@ -5,7 +5,13 @@ import Col from 'react-bootstrap/lib/Col'; import { Tooltip, OverlayTrigger } from 'react-bootstrap'; // Redux -import { start, stop, toggleRepeat, toggleScroll, buildOnClient } from '../../redux/modules/audioplayer'; +import { + start, + stop, + toggleRepeat, + toggleScroll, + buildOnClient +} from '../../redux/modules/audioplayer'; import { setCurrentAyah, setCurrentWord } from '../../redux/modules/ayahs'; // Components @@ -64,7 +70,7 @@ export default class Audioplayer extends Component { className: PropTypes.string, surah: PropTypes.object.isRequired, onLoadAyahs: PropTypes.func.isRequired, - + segments: PropTypes.object, files: PropTypes.object, currentAyah: PropTypes.string, currentWord: PropTypes.string, @@ -79,7 +85,8 @@ export default class Audioplayer extends Component { stop: PropTypes.func.isRequired, toggleRepeat: PropTypes.func.isRequired, toggleScroll: PropTypes.func.isRequired, - ayahIds: PropTypes.array + ayahIds: PropTypes.array, + isStarted: PropTypes.bool }; static defaultProps = { @@ -94,117 +101,124 @@ export default class Audioplayer extends Component { componentDidMount() { const { isLoadedOnClient, buildOnClient, surah } = this.props; // eslint-disable-line no-shadow + debug('component:Audioplayer', 'componentDidMount'); if (!isLoadedOnClient && __CLIENT__) { - //console.debug('Audioplayer componentDidMount'); debug('component:Audioplayer', 'componentDidMount on client'); + return buildOnClient(surah.id); - }// else console.debug('Audioplayer componentDidMount', { notLoadedOnClient: !isLoadedOnClient, client: __CLIENT__ }); + } + + return false; } componentWillUnmount() { debug('component:Audioplayer', 'componentWillUnmount'); - //console.log('Audioplayer componentWillUnmount'); - this.stop(); + + return this.stop(); + } + + getCurrent() { + const { currentAyah, ayahIds } = this.props; + const index = ayahIds.findIndex(id => id === currentAyah); + + return ayahIds[index]; + } + + getPrevious() { + // TODO BUGFIX, we should be able to go to the previous ayah even when + // we started from within a range + // i.e. lazyloading upwards; as this is defined, if you go to /2/100-110 + // then you can't go to 99 from + // the previous button + const { currentAyah, ayahIds } = this.props; + const index = ayahIds.findIndex(id => id === currentAyah) - 1; + return ayahIds[index]; } - onPreviousAyah() { + getNext() { + const { currentAyah, ayahIds, onLoadAyahs } = this.props; + const index = ayahIds.findIndex(id => id === currentAyah) + 1; + + if ((ayahIds.length - 3) <= index) { + onLoadAyahs(); // this doesnt look right, should probably be returned or promise.then? + } + + return ayahIds[index]; + } + + handlePreviousAyah = () => { const { setCurrentAyah, isStarted, shouldScroll } = this.props; // eslint-disable-line no-shadow const prevAyah = this.getPrevious(); if (prevAyah) { - const ayahNum = prevAyah.replace( /^\d+:/, '' ); + const ayahNum = prevAyah.replace(/^\d+:/, ''); setCurrentAyah(prevAyah); if (shouldScroll) { - scroller.scrollTo('ayah:'+ ayahNum, -150); + scroller.scrollTo(`ayah:${ayahNum}`, -150); } - if (isStarted) - this.props.files[prevAyah].play(); - } - } + if (isStarted) return this.props.files[prevAyah].play(); - scrollTo(name, offset = 0) { - const node = document.getElementsByName(name)[0]; - - if (!node) { - return; + return false; } - const nodeRect = node.getBoundingClientRect(); - const bodyRect = document.body.getBoundingClientRect(); - const scrollOffset = nodeRect.top - bodyRect.top; - - window.scrollTo(0, scrollOffset + offset); + return false; } - onNextAyah() { + handleNextAyah = () => { const { setCurrentAyah, isStarted, shouldScroll } = this.props; // eslint-disable-line no-shadow - const file = this.props.files[this.props.currentAyah]; - const nextAyah = this.getNext(); if (!nextAyah) return this.stop(); - const ayahNum = nextAyah.replace( /^\d+:/, '' ); + const ayahNum = nextAyah.replace(/^\d+:/, ''); setCurrentAyah(nextAyah); if (shouldScroll) { - scroller.scrollTo('ayah:'+ ayahNum, -80); + scroller.scrollTo(`ayah:${ayahNum}`, -80); } this.preloadNext(); - if (isStarted) this.props.files[nextAyah].play(); - } - getCurrent() { - const { currentAyah, ayahIds } = this.props; - const index = ayahIds.findIndex(id => id === currentAyah); + if (isStarted) return this.props.files[nextAyah].play(); - return ayahIds[index]; + return false; } - getPrevious() { - // TODO BUGFIX, we should be able to go to the previous ayah even when we started from within a range - // i.e. lazyloading upwards; as this is defined, if you go to /2/100-110 then you can't go to 99 from - // the previous button - const { currentAyah, ayahIds } = this.props; - const index = ayahIds.findIndex(id => id === currentAyah) - 1; - //console.debug('getPrevious', { props: this.props, index, prevAyah: ayahIds[index], currentAyah }) - return ayahIds[index]; - } + scrollTo(name, offset = 0) { + const node = document.getElementsByName(name)[0]; - getNext() { - const { currentAyah, ayahIds, onLoadAyahs } = this.props; - const index = ayahIds.findIndex(id => id === currentAyah) + 1; + if (!node) return; - if ((ayahIds.length - 3) <= index) { - onLoadAyahs(); // this doesnt look right, should probably be returned or promise.then? - } + const nodeRect = node.getBoundingClientRect(); + const bodyRect = document.body.getBoundingClientRect(); + const scrollOffset = nodeRect.top - bodyRect.top; - //console.debug('getNext', { props: this.props, index, nextAyah: ayahIds[index], currentAyah }) - return ayahIds[index]; + window.scrollTo(0, scrollOffset + offset); } - startStopPlayer(event) { + handleStartStopPlayer = (event) => { event.preventDefault(); const { isStarted } = this.props; - if (isStarted) + if (isStarted) { return this.stop(); + } + return this.start(); } start() { - const { shouldScroll, files } = this.props; + const { shouldScroll } = this.props; const currentAyah = this.getCurrent(); - const ayahNum = currentAyah.replace( /^\d+:/, '' ); + const ayahNum = currentAyah.replace(/^\d+:/, ''); if (shouldScroll) { - scroller.scrollTo('ayah:'+ ayahNum, -150); + scroller.scrollTo(`ayah:${ayahNum}`, -150); } this.props.start(); @@ -218,7 +232,7 @@ export default class Audioplayer extends Component { preloadNext() { const { currentAyah, ayahIds, files } = this.props; const index = ayahIds.findIndex(id => id === currentAyah) + 1; - for (var i = index; i <= index + 2; i++) { + for (let i = index; i <= index + 2; i++) { if (ayahIds[i]) { const ayahKey = ayahIds[i]; if (files[ayahKey]) { @@ -228,25 +242,19 @@ export default class Audioplayer extends Component { } } - toggleRepeat(event) { - event.preventDefault(); - - this.props.toggleRepeat(); - } - - toggleScroll(event) { + handleToggleScroll = (event) => { event.preventDefault(); const { shouldScroll } = this.props; const currentAyah = this.getCurrent(); - const ayahNum = currentAyah.replace( /^\d+:/, '' ); + const ayahNum = currentAyah.replace(/^\d+:/, ''); if (!shouldScroll) { // we use the inverse (!) here because we're toggling, so false is true - const elem = document.getElementsByName('ayah:'+ ayahNum)[0]; + const elem = document.getElementsByName(`ayah:${ayahNum}`)[0]; if (elem && elem.getBoundingClientRect().top < 0) { // if the ayah is above our scroll offset - scroller.scrollTo('ayah:'+ ayahNum, -150); + scroller.scrollTo(`ayah:${ayahNum}`, -150); } else { - scroller.scrollTo('ayah:'+ ayahNum, -80); + scroller.scrollTo(`ayah:${ayahNum}`, -80); } } @@ -277,7 +285,7 @@ export default class Audioplayer extends Component { } return ( - + {icon} ); @@ -288,7 +296,10 @@ export default class Audioplayer extends Component { const index = ayahIds.findIndex(id => id === currentAyah); return ( - + ); @@ -296,15 +307,19 @@ export default class Audioplayer extends Component { renderNextButton() { return ( - + ); } renderRepeatButton() { - const { shouldRepeat } = this.props; - const tooltip = (Repeats the current ayah on end...); + const { shouldRepeat, toggleRepeat } = this.props; // eslint-disable-line no-shadow + const tooltip = ( + + Repeats the current ayah on end... + + ); return ( @@ -314,12 +329,13 @@ export default class Audioplayer extends Component { placement="right" trigger={['hover', 'focus']} > - + ); @@ -327,7 +343,11 @@ export default class Audioplayer extends Component { renderScrollButton() { const { shouldScroll } = this.props; - const tooltip = (Automatically scrolls to the currently playing ayah on transitions...); + const tooltip = ( + + Automatically scrolls to the currently playing ayah on transitions... + + ); return ( @@ -337,12 +357,13 @@ export default class Audioplayer extends Component { placement="right" trigger={['hover', 'focus']} > - + ); @@ -358,13 +379,13 @@ export default class Audioplayer extends Component { segments, currentAyah, currentWord, - setCurrentWord, + setCurrentWord, // eslint-disable-line no-shadow isStarted, shouldRepeat, isSupported, isLoadedOnClient, surah - } = this.props; // eslint-disable-line no-shadow + } = this.props; if (!isSupported) { return ( @@ -409,8 +430,8 @@ export default class Audioplayer extends Component { condition); + return conditions.some(condition => condition); + } + + handleWordClick = (event) => { + if (event.target && /^word-/.test(event.target.id)) { + // call onWordClick in Surah + this.props.onWordClick(event.target.id.match(/\d+/g).join(':')); + } + } + + handleWordFocus = (event) => { + if (event.target && /^word-/.test(event.target.id)) { + // call onWordFocus in Surah + this.props.onWordFocus(event.target.id.match(/\d+/g).join(':'), event.target); + } + } + + handlePlay() { + this.setState({ + open: false + }); } renderTranslations() { const { ayah, match } = this.props; - const array = match ? match : ayah.content || []; + const array = match || ayah.content || []; return array.map((content, index) => { const arabic = new RegExp(/[\u0600-\u06FF]/); @@ -50,7 +74,10 @@ export default class Ayah extends Component { const isArabic = arabic.test(character); return ( -
+

{content.name || content.resource.name}

@@ -60,33 +87,20 @@ export default class Ayah extends Component { }); } - onWordClick(event) { - if (event.target && /^word-/.test(event.target.id)) { - // call onWordClick in Surah - this.props.onWordClick(event.target.id.match(/\d+/g).join(':')); - } - } - - onWordFocus(event) { - if (event.target && /^word-/.test(event.target.id)) { - // call onWordFocus in Surah - this.props.onWordFocus(event.target.id.match(/\d+/g).join(':'), event.target); - } - } - renderText() { - if (!this.props.ayah.words[0].code) { - return; + const { ayah, currentWord } = this.props; + + if (!ayah.words[0].code) { + return false; } - const { currentWord } = this.props; let position = 0; - let text = this.props.ayah.words.map(word => { + let text = ayah.words.map(word => { let id = null; - let active = word.charTypeId == CHAR_TYPE_WORD && currentWord === position ? true : false; - let className = `${word.className}${word.highlight? ' '+word.highlight : ''}${active? ' '+ styles.active : ''}`; + const active = word.charTypeId === CHAR_TYPE_WORD && currentWord === position; + const className = `${word.className} ${word.highlight && word.highlight} ${active && styles.active}`; // eslint-disable-line max-len - if (word.charTypeId == CHAR_TYPE_WORD) { + if (word.charTypeId === CHAR_TYPE_WORD) { id = `word-${word.ayahKey.replace(/:/, '-')}-${position++}`; } else { id = `${word.className}-${word.codeDec}`; // just don't include id @@ -99,68 +113,69 @@ export default class Ayah extends Component { ); } - else { - return ( - - ); - } + + return ( + + ); }); return (

{text} +

); } - goToAyah(ayah, e) { - e.preventDefault(); - - this.setState({ - open: false - }); - } - - handleCopy = () => { - const { ayah } = this.props; - - CopyToClipboard(ayah.textTashkeel); - } - renderPlayLink() { - if (!this.props.isSearch) { - - Play - + const { isSearch, ayah } = this.props; + + if (!isSearch) { + return ( + this.handlePlay(ayah.ayahNum)} + className="text-muted" + > + Play + + ); } + + return false; } renderCopyLink() { - const { isSearch, ayah: { textTashkeel } } = this.props; + const { isSearched, ayah: { textTashkeel } } = this.props; - if (!isSearch) { + if (!isSearched) { return ( ); } + + return false; } renderAyahBadge() { @@ -177,7 +192,8 @@ export default class Ayah extends Component { return ( + data-metrics-event-name="Ayah:Searched:Link" + > {content} ); @@ -186,10 +202,6 @@ export default class Ayah extends Component { return content; } - shareDialog(href) { - window.open(href, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=300,width=600') - } - renderControls() { return (
@@ -202,7 +214,7 @@ export default class Ayah extends Component { render() { const { ayah } = this.props; - debug(`component:Ayah`, `Render ${this.props.ayah.ayahNum}`); + debug('component:Ayah', `Render ${this.props.ayah.ayahNum}`); return ( @@ -215,4 +227,3 @@ export default class Ayah extends Component { ); } } - diff --git a/src/components/Bismillah/index.js b/src/components/Bismillah/index.js index 2488eeb0d..a1e39aa31 100644 --- a/src/components/Bismillah/index.js +++ b/src/components/Bismillah/index.js @@ -5,7 +5,8 @@ const Bismillah = ({ surah }) => { return (
+ style={{textAlign: 'center'}} + > ï­‘ï­’ï­“
); diff --git a/src/components/ContentDropdown/index.js b/src/components/ContentDropdown/index.js index 84b6a59e6..01b1822fb 100644 --- a/src/components/ContentDropdown/index.js +++ b/src/components/ContentDropdown/index.js @@ -1,6 +1,5 @@ import React, { Component, PropTypes } from 'react'; -import DropdownButton from 'react-bootstrap/lib/DropdownButton'; import MenuItem from 'react-bootstrap/lib/MenuItem'; const style = require('./style.scss'); @@ -472,7 +471,7 @@ export default class ContentDropdown extends Component { type="checkbox" className={style.checkbox} id={slug.id + slug.language} - onChange={this.handleOptionSelected.bind(this, slug.id)} + onChange={() => this.handleOptionSelected(slug.id)} checked={checked} /> @@ -502,23 +501,24 @@ export default class ContentDropdown extends Component { return (
    { content.length && - Remove all + Remove all } English {this.renderEnglishList()} - + Other Languages {this.renderLanguagesList()}
diff --git a/src/components/Copy/index.js b/src/components/Copy/index.js index 78a5dbed0..ab407ccfa 100644 --- a/src/components/Copy/index.js +++ b/src/components/Copy/index.js @@ -1,5 +1,5 @@ import React, { Component, PropTypes } from 'react'; -import CopyToClipboard from 'copy-to-clipboard'; +import copyToClipboard from 'copy-to-clipboard'; export default class Copy extends Component { static propTypes = { @@ -11,7 +11,7 @@ export default class Copy extends Component { }; handleCopy = () => { - CopyToClipboard(this.props.text); + copyToClipboard(this.props.text); this.setState({isCopied: true}); setTimeout(() => this.setState({isCopied: false}), 1000); @@ -24,7 +24,8 @@ export default class Copy extends Component { + data-metrics-event-name="Ayah:Copy" + > {isCopied ? 'Copied!' : 'Copy'} ); diff --git a/src/components/FontSizeDropdown/index.js b/src/components/FontSizeDropdown/index.js index ae09bd165..a17fc75a4 100644 --- a/src/components/FontSizeDropdown/index.js +++ b/src/components/FontSizeDropdown/index.js @@ -1,8 +1,8 @@ import React, { Component, PropTypes } from 'react'; -import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger' -import Popover from 'react-bootstrap/lib/Popover' -import Row from 'react-bootstrap/lib/Row' +import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; +import Popover from 'react-bootstrap/lib/Popover'; +import Row from 'react-bootstrap/lib/Row'; import Col from 'react-bootstrap/lib/Col'; const style = require('./style.scss'); @@ -29,14 +29,11 @@ export default class FontSizeDropdown extends Component { } renderPopup() { - const incrementValueArabic = 5; - const incrementValueTranslation = 2; - return ( - + this.handleOptionSelected('arabic', -1)} className="pointer"> @@ -44,15 +41,15 @@ export default class FontSizeDropdown extends Component { Arabic - - + this.handleOptionSelected('arabic', 1)} className="pointer"> + -
+
- + this.handleOptionSelected('translation', -1)} className="pointer"> @@ -60,8 +57,8 @@ export default class FontSizeDropdown extends Component { Translations - - + this.handleOptionSelected('translation', 1)} className="pointer"> + @@ -75,7 +72,8 @@ export default class FontSizeDropdown extends Component { + data-metrics-event-name="FontSizeDropdown" + > Font size diff --git a/src/components/FontStyles/index.js b/src/components/FontStyles/index.js index 96431ac24..90face967 100644 --- a/src/components/FontStyles/index.js +++ b/src/components/FontStyles/index.js @@ -1,46 +1,55 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; +import { fontFaceStyle, fontFaceStyleLoaded } from '../../helpers/buildFontFaces'; +import { load } from 'redux/modules/fontFaces'; -const bismillah = `@font-face {font-family: 'bismillah'; - src: url('http://quran-1f14.kxcdn.com/fonts/ttf/bismillah.ttf') format('truetype')} - .bismillah{font-family: 'bismillah'; font-size: 36px !important; color: #000; padding: 25px 0px;}`; - -class FontStyle extends Component { - static propTypes = { - fontFace: PropTypes.string.isRequired - } - - static defaultProps = { - fontFace: '' - } - - shouldComponentUpdate(nextProps) { - return this.props.fontFace !== nextProps.fontFace; - } - - render() { - return