diff --git a/.babelrc b/.babelrc index a178a812b..aef13b0f4 100644 --- a/.babelrc +++ b/.babelrc @@ -2,7 +2,6 @@ "presets": ["react", "es2015", "stage-0"], "plugins": [ "transform-runtime", - "add-module-exports", "transform-decorators-legacy", "transform-react-display-name", ["system-import-transformer", {"modules": "common"}] diff --git a/.gitignore b/.gitignore index 707bb3209..54d5ca4a0 100644 --- a/.gitignore +++ b/.gitignore @@ -11,8 +11,7 @@ webpack-stats.json webpack-assets.json bundle-stats.json selenium-debug.log -tests/functional/output/* -test/functional/screenshots/* .ssh webpack-stats.debug.json *.DS_Store +.vscode diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e35fbfe63..4b545569f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,10 +55,6 @@ Pull requests are the greatest contributions, so be sure they are focused in sco ``` Our `npm run test` tests and _watch_. Otherwise use `npm run test:ci:unit` for CI level tests. - We also have nightwatch function tests. You can install nightwatch globally and can run tests like this: - ``` - nightwatch --test tests/functional/specs/Index_spec.js - ``` 5. To create a PR you need to push your branch to the origin(forked) remote and then press some buttons on GitHub: diff --git a/README.md b/README.md index 3a973e5f6..fe595b07c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Quran.com +# Quran.com [![SLACK](http://i.imgur.com/Lk5HsBo.png)](https://quranslack.herokuapp.com) This project is the frontend for Quran.com. It is built using [Reactjs] + [Redux] + [Expressjs] + [Webpack]. It is isomorphic (javascript shared diff --git a/bin/server.js b/bin/server.js index d15b1ea68..303749064 100644 --- a/bin/server.js +++ b/bin/server.js @@ -34,5 +34,5 @@ if (__DEVELOPMENT__) { global.webpack_isomorphic_tools = new webpackIsomorphicTools(require('../webpack/isomorphic-tools-configuration')) .development(process.env.NODE_ENV === 'development') .server(rootDir, function() { - require('../src/server.js')(); + require('../src/server.js').default(); }); diff --git a/bin/server.prod.js b/bin/server.prod.js index 6695c6c4f..880fbbfa5 100644 --- a/bin/server.prod.js +++ b/bin/server.prod.js @@ -13,5 +13,5 @@ global.__DEVELOPMENT__ = process.env.NODE_ENV !== 'production'; global.webpack_isomorphic_tools = new webpackIsomorphicTools(require('../webpack/isomorphic-tools-configuration')) .development(__DEVELOPMENT__) .server(rootDir, function() { - require('../dist/server.js')(); + require('../dist/server.js').default(); }); diff --git a/package.json b/package.json index 0d4313466..044377b8e 100644 --- a/package.json +++ b/package.json @@ -7,15 +7,12 @@ "test": "npm run test:dev:unit", "test:ci:unit": "karma start --browsers PhantomJS --single-run", "pretest:ci:unit": "npm run test:ci:lint", - "test:ci:functional": "BROWSER=phantomjs ./tests/functional/test.sh start-ci", - "posttest:ci:functional": "./tests/functional/test.sh stop", "test:dev:unit": "karma start", "test:ci:lint": "./node_modules/eslint/bin/eslint.js ./src", - "test:dev:functional": "BROWSER=chrome ./tests/functional/test.sh start", - "posttest:dev:functional": "./tests/functional/test.sh stop", "test:dev:lint": "./node_modules/eslint/bin/eslint.js ./src", "test:stylelint": "stylelint './src/**/*.scss' --config ./webpack/.stylelintrc", - "dev": "env NODE_PATH='./src' PORT=8000 UV_THREADPOOL_SIZE=100 node ./webpack/webpack-dev-server.js & env NODE_PATH='./src' PORT=8000 node ./bin/server.js", + "dev-old": "env NODE_PATH='./src' PORT=8000 UV_THREADPOOL_SIZE=100 node ./webpack/webpack-dev-server.js & env NODE_PATH='./src' PORT=8000 node ./bin/server.js", + "dev": "env NODE_PATH='./src' PORT=8000 UV_THREADPOOL_SIZE=100 node ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --config ./webpack/dev.config.js & env NODE_PATH='./src' PORT=8000 node ./bin/server.js", "start": "NODE_PATH='src' node ./start", "build": "npm run build:client & npm run build:server", "build:server": "babel ./src -d ./dist -D", @@ -29,12 +26,14 @@ }, "dependencies": { "app-module-path": "1.0.2", + "autoprefixer": "6.6.1", "autoprefixer-loader": "3.2.0", "babel-cli": "6.11.4", - "babel-core": "6.13.2", - "babel-loader": "6.2.4", + "babel-core": "^6.24.0", + "babel-loader": "^6.4.1", "babel-plugin-add-module-exports": "0.1.4", - "babel-plugin-system-import-transformer": "2.2.1", + "babel-plugin-syntax-dynamic-import": "^6.18.0", + "babel-plugin-system-import-transformer": "^3.1.0", "babel-plugin-transform-decorators-legacy": "1.3.4", "babel-plugin-transform-react-constant-elements": "6.9.1", "babel-plugin-transform-react-display-name": "6.8.0", @@ -43,46 +42,52 @@ "babel-plugin-transform-runtime": "6.12.0", "babel-plugin-typecheck": "3.9.0", "babel-polyfill": "6.13.0", - "babel-preset-es2015": "6.13.2", + "babel-preset-es2015": "^6.24.0", "babel-preset-react": "6.11.1", "babel-preset-stage-0": "6.5.0", + "babel-preset-stage-2": "6.13.0", "babel-register": "6.11.6", "babel-runtime": "6.11.6", "body-parser": "1.15.2", - "bootstrap-loader": "1.1.0", + "bootstrap-loader": "2.0.0-beta.19", "bootstrap-sass": "3.3.7", - "bootstrap-sass-loader": "1.0.10", "bundle-loader": "0.5.4", "cache-manager": "1.5.0", + "caniuse-db": "1.0.30000613", "clean-webpack-plugin": "0.1.10", "compression": "1.6.2", "compression-webpack-plugin": "0.3.2", "cookie-parser": "1.4.3", "copy-to-clipboard": "3.0.5", "cors": "2.7.1", - "css-loader": "0.23.1", + "css-loader": "0.26.1", + "cssnano": "3.10.0", "debug": "2.2.0", "dotenv": "2.0.0", "errorhandler": "1.4.3", "express": "4.14.0", "express-state": "1.4.0", "express-useragent": "1.0.4", - "extract-text-webpack-plugin": "2.0.0-beta.3", + "extract-text-webpack-plugin": "2.0.0-beta.5", "file-loader": "0.8.5", "fontfaceobserver": "1.7.3", "history": "^3.0.0", "html-webpack-plugin": "1.7.0", "http-proxy": "1.14.0", "humps": "2.0.0", + "image-webpack-loader": "3.1.0", "imports-loader": "0.6.5", "json-loader": "0.5.4", "morgan": "1.7.0", "node-sass": "4.1.1", "normalizr": "3.0.2", + "postcss-loader": "0.9.1", + "precss": "1.4.0", "pretty-error": "2.0.0", "promise": "7.1.1", "proxy-middleware": "0.14.0", "qs": "6.2.1", + "quran-components": "^0.0.66", "raven": "1.1.1", "raw-loader": "0.5.1", "react": "15.4.1", @@ -92,8 +97,10 @@ "react-cookie": "1.0.4", "react-dom": "15.4.1", "react-helmet": "3.1.0", + "react-hot-loader": "3.0.0-beta.6", "react-inlinesvg": "0.5.4", "react-intl": "2.1.5", + "react-loadable": "^3.0.1", "react-metrics": "1.2.1", "react-paginate": "4.1.0", "react-redux": "5.0.1", @@ -108,6 +115,7 @@ "redux-connect": "5.0.0", "reselect": "2.5.3", "resolve-url": "0.2.1", + "resolve-url-loader": "1.6.1", "sass-loader": "4.1.1", "serialize-javascript": "1.3.0", "serve-favicon": "2.3.0", @@ -117,7 +125,8 @@ "superagent": "3.3.1", "url": "0.11.0", "url-loader": "0.5.7", - "webpack": "2.1.0-beta.20", + "webpack": "2.2.0", + "webpack-dev-server": "2.1.0-beta.0", "webpack-isomorphic-tools": "2.5.7", "winston": "1.1.2" }, @@ -163,16 +172,12 @@ "redux-devtools": "3.1.1", "redux-devtools-dock-monitor": "1.1.0", "redux-devtools-log-monitor": "1.0.5", - "selenium-server": "2.48.2", "sinon": "1.15.3", "sinon-chai": "2.8.0", "stylelint": "7.1.0", "stylelint-webpack-plugin": "0.2.0", - "wdio-mocha-framework": "0.3.7", - "wdio-spec-reporter": "0.0.3", - "webdriverio": "4.2.1", "webpack-bundle-analyzer": "2.2.1", - "webpack-dev-server": "1.6.5", + "webpack-dev-server": "2.1.0-beta.0", "webpack-hot-middleware": "2.12.2" }, "pre-commit": [ diff --git a/src/components/Audioplayer/RepeatDropdown/index.js b/src/components/Audioplayer/RepeatDropdown/index.js index d4f0379d7..6b58449c1 100644 --- a/src/components/Audioplayer/RepeatDropdown/index.js +++ b/src/components/Audioplayer/RepeatDropdown/index.js @@ -1,31 +1,17 @@ import React, { Component, PropTypes } from 'react'; +import * as customPropTypes from 'customPropTypes'; import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; import Popover from 'react-bootstrap/lib/Popover'; import Nav from 'react-bootstrap/lib/Nav'; import NavItem from 'react-bootstrap/lib/NavItem'; import FormControl from 'react-bootstrap/lib/FormControl'; -import Col from 'react-bootstrap/lib/Col'; import { intlShape, injectIntl } from 'react-intl'; - import SwitchToggle from 'components/SwitchToggle'; import LocaleFormattedMessage from 'components/LocaleFormattedMessage'; -import surahType from 'types/surahType'; - const style = require('../style.scss'); class RepeatButton extends Component { - static propTypes = { - surah: surahType, - repeat: PropTypes.shape({ - from: PropTypes.number, - to: PropTypes.number, - times: PropTypes.number - }).isRequired, - setRepeat: PropTypes.func.isRequired, - current: PropTypes.number.isRequired, - intl: intlShape.isRequired - }; handleToggle = () => { const { repeat, setRepeat, current } = this.props; @@ -38,7 +24,7 @@ class RepeatButton extends Component { from: current, to: current }); - } + }; handleNavChange = (nav) => { const { setRepeat, current } = this.props; @@ -58,11 +44,11 @@ class RepeatButton extends Component { } renderRangeAyahs() { - const { surah, repeat, setRepeat } = this.props; - const array = Array(surah.ayat).join().split(','); + const { chapter, repeat, setRepeat } = this.props; + const array = Array(chapter.versesCount).join().split(','); return ( - +
- +
); } renderSingleAyah() { - const { repeat, setRepeat, surah } = this.props; - const array = Array(surah.ayat).join().split(','); + const { repeat, setRepeat, chapter } = this.props; + const array = Array(chapter.versesCount).join().split(','); return ( - +
{' '}:
- +
); } @@ -150,7 +150,7 @@ class RepeatButton extends Component { return (
- +
- +
); } @@ -190,7 +190,7 @@ class RepeatButton extends Component { return (
- +
- +
); } @@ -230,7 +230,7 @@ class RepeatButton extends Component { className={style.popover} title={
- +
- +
} > @@ -268,4 +268,12 @@ class RepeatButton extends Component { } } +RepeatButton.propTypes = { + chapter: customPropTypes.surahType, + repeat: customPropTypes.timeInterval, + setRepeat: PropTypes.func.isRequired, + current: PropTypes.number.isRequired, + intl: intlShape.isRequired +}; + export default injectIntl(RepeatButton); diff --git a/src/components/Audioplayer/RepeatDropdown/spec.js b/src/components/Audioplayer/RepeatDropdown/spec.js index 7d89fb2ed..578bed164 100644 --- a/src/components/Audioplayer/RepeatDropdown/spec.js +++ b/src/components/Audioplayer/RepeatDropdown/spec.js @@ -6,7 +6,7 @@ import RepeatDropdown from './index'; let component; let overlay; let setRepeat; -const surah = { +const chapter = { ayat: 10 }; @@ -18,7 +18,7 @@ const makeComponent = (repeat) => { repeat={repeat} setRepeat={setRepeat} current={1} - surah={surah} + chapter={chapter} /> ); diff --git a/src/components/Audioplayer/ScrollButton/index.js b/src/components/Audioplayer/ScrollButton/index.js index cbbce363d..91d3d9b97 100644 --- a/src/components/Audioplayer/ScrollButton/index.js +++ b/src/components/Audioplayer/ScrollButton/index.js @@ -7,7 +7,7 @@ const style = require('../style.scss'); const ScrollButton = ({ shouldScroll, onScrollToggle }) => { const tooltip = ( - + test); } render() { - const { segments, currentAyah, currentTime } = this.props; + const { segments, currentVerse, currentTime } = this.props; const style = []; let currentWord = null; @@ -29,7 +22,7 @@ export default class Segments extends Component { const word = segments.words[wordIndex]; if (currentTime >= word.startTime && currentTime < word.endTime) { - currentWord = `${currentAyah}:${wordIndex}`; + currentWord = `${currentVerse}:${wordIndex}`; } }); @@ -53,3 +46,11 @@ export default class Segments extends Component { ); } } + +Segments.propTypes = { + segments: customPropTypes.segments.isRequired, + currentVerse: PropTypes.string, + currentTime: PropTypes.number +}; + +export default Segments; diff --git a/src/components/Audioplayer/Segments/spec.js b/src/components/Audioplayer/Segments/spec.js index be3b3fd5e..6dfca9a12 100644 --- a/src/components/Audioplayer/Segments/spec.js +++ b/src/components/Audioplayer/Segments/spec.js @@ -29,7 +29,7 @@ describe('', () => { ); }); @@ -64,7 +64,7 @@ describe('', () => { } }} currentTime={1.5} - currentAyah="1:1" + currentVerse="1:1" /> ); }); diff --git a/src/components/Audioplayer/Track/index.js b/src/components/Audioplayer/Track/index.js index 619403bdd..74c0c92b7 100644 --- a/src/components/Audioplayer/Track/index.js +++ b/src/components/Audioplayer/Track/index.js @@ -18,7 +18,7 @@ export default class Track extends Component { ); return onTrackChange(fraction); - } + }; render() { const { progress } = this.props; diff --git a/src/components/Audioplayer/index.js b/src/components/Audioplayer/index.js index 7f5f6354f..b7065fb48 100644 --- a/src/components/Audioplayer/index.js +++ b/src/components/Audioplayer/index.js @@ -1,69 +1,42 @@ /* global document */ // TODO: This file is too too large. import React, { Component, PropTypes } from 'react'; +import * as customPropTypes from 'customPropTypes'; import { connect } from 'react-redux'; import { camelize } from 'humps'; - +import Loadable from 'react-loadable'; import LocaleFormattedMessage from 'components/LocaleFormattedMessage'; // Helpers import debug from 'helpers/debug'; import scroller from 'utils/scroller'; -import { surahType, segmentType } from 'types'; - // Redux import * as AudioActions from 'redux/actions/audioplayer'; +import ComponentLoader from 'components/ComponentLoader'; import Track from './Track'; import Segments from './Segments'; import ScrollButton from './ScrollButton'; -import RepeatDropdown from './RepeatDropdown'; const style = require('./style.scss'); +const RepeatDropdown = Loadable({ + loader: () => import('./RepeatDropdown'), + LoadingComponent: ComponentLoader +}); + export class Audioplayer extends Component { - static propTypes = { - className: PropTypes.string, - surah: surahType, - onLoadAyahs: PropTypes.func.isRequired, - segments: PropTypes.objectOf(segmentType), - // NOTE: should be PropTypes.instanceOf(Audio) but not on server. - files: PropTypes.object, // eslint-disable-line - currentAyah: PropTypes.string, - buildOnClient: PropTypes.func.isRequired, - isLoadedOnClient: PropTypes.bool.isRequired, - isLoading: PropTypes.bool.isRequired, - play: PropTypes.func.isRequired, - pause: PropTypes.func.isRequired, - next: PropTypes.func.isRequired, // eslint-disable-line - previous: PropTypes.func.isRequired, // eslint-disable-line - update: PropTypes.func.isRequired, - repeat: PropTypes.shape({ - from: PropTypes.number, - to: PropTypes.number, - time: PropTypes.number, - }).isRequired, - shouldScroll: PropTypes.bool.isRequired, - setRepeat: PropTypes.func.isRequired, - setAyah: PropTypes.func.isRequired, - toggleScroll: PropTypes.func.isRequired, - isPlaying: PropTypes.bool, - currentTime: PropTypes.number, - duration: PropTypes.number, - // NOTE: should be PropTypes.instanceOf(Audio) but not on server. - currentFile: PropTypes.any // eslint-disable-line - }; componentDidMount() { - const { isLoadedOnClient, buildOnClient, surah, currentFile } = this.props; // eslint-disable-line no-shadow, max-len + const { isLoadedOnClient, buildOnClient, chapter, currentFile } = this.props; // eslint-disable-line no-shadow, max-len debug('component:Audioplayer', 'componentDidMount'); if (!isLoadedOnClient && __CLIENT__) { debug('component:Audioplayer', 'componentDidMount on client'); - return buildOnClient(surah.id); + return buildOnClient(chapter.chapterNumber); } if (currentFile) { @@ -75,16 +48,16 @@ export class Audioplayer extends Component { componentWillReceiveProps(nextProps) { // Make sure we have a current ayah to mount it to Audio - if (!this.props.currentAyah && !nextProps.currentFile) { + if (!this.props.currentVerse && !nextProps.currentFile) { return false; } - // When you go directly to the surah page, /2, the files are not loaded yet - if (this.props.isLoadedOnClient !== nextProps.isLoadedOnClient) { + // First load + if (this.props.currentFile !== nextProps.currentFile) { return this.handleAddFileListeners(nextProps.currentFile); } - if (this.props.currentAyah !== nextProps.currentAyah) { + if (this.props.currentVerse !== nextProps.currentVerse) { this.handleAddFileListeners(nextProps.currentFile); if (this.props.currentFile) { @@ -97,6 +70,20 @@ export class Audioplayer extends Component { return false; } + componentDidUpdate() { + const { currentFile, isPlaying } = this.props; + + if (!currentFile) return false; + + if (isPlaying) { + currentFile.play(); + } else { + currentFile.pause(); + } + + return false; + } + componentWillUnmount() { const { files, currentFile } = this.props; debug('component:Audioplayer', 'componentWillUnmount'); @@ -109,21 +96,21 @@ export class Audioplayer extends Component { } getPrevious() { - const { currentAyah, files } = this.props; + const { currentVerse, files } = this.props; const ayahIds = Object.keys(files); - const index = ayahIds.findIndex(id => id === currentAyah); + const index = ayahIds.findIndex(id => id === currentVerse); return ayahIds[index - 1]; } getNext() { - const { currentAyah, surah, files, onLoadAyahs } = this.props; + const { currentVerse, chapter, files, onLoadAyahs } = this.props; const ayahIds = Object.keys(files); - const ayahNum = currentAyah.split(':')[1]; - const index = ayahIds.findIndex(id => id === currentAyah); + const ayahNum = currentVerse.split(':')[1]; + const index = ayahIds.findIndex(id => id === currentVerse); - if (surah.ayat === ayahNum + 1) { - // We are at the end of the surah! + if (chapter.versesCount === ayahNum + 1) { + // We are at the end of the chapter! return false; } @@ -136,16 +123,17 @@ export class Audioplayer extends Component { } handleAyahChange = (direction = 'next') => { - const { isPlaying, play, pause, currentAyah } = this.props; // eslint-disable-line no-shadow, max-len + const { isPlaying, play, pause, currentVerse } = this.props; // eslint-disable-line no-shadow, max-len const previouslyPlaying = isPlaying; if (isPlaying) pause(); - if (!this[camelize(`get_${direction}`)]()) return pause(); + const nextVerse = this[camelize(`get_${direction}`)](); + if (!nextVerse) return pause(); - this.props[direction](currentAyah); + this.props[direction](currentVerse); - this.handleScrollTo(currentAyah); + this.handleScrollTo(nextVerse); this.preloadNext(); @@ -154,11 +142,15 @@ export class Audioplayer extends Component { return false; } - handleScrollTo = (ayahNum = this.props.currentAyah) => { + scrollToVerse = (ayahNum = this.props.currentVerse) => { + scroller.scrollTo(`verse:${ayahNum}`, -45); + } + + handleScrollTo = (ayahNum) => { const { shouldScroll } = this.props; if (shouldScroll) { - scroller.scrollTo(`ayah:${ayahNum}`, -150); + this.scrollToVerse(ayahNum); } } @@ -170,16 +162,16 @@ export class Audioplayer extends Component { } preloadNext() { - const { currentAyah, files } = this.props; + const { currentVerse, files } = this.props; const ayahIds = Object.keys(files); - const index = ayahIds.findIndex(id => id === currentAyah) + 1; + const index = ayahIds.findIndex(id => id === currentVerse) + 1; for (let id = index; id <= index + 2; id += 1) { if (ayahIds[id]) { - const ayahKey = ayahIds[id]; + const verseKey = ayahIds[id]; - if (files[ayahKey]) { - files[ayahKey].setAttribute('preload', 'auto'); + if (files[verseKey]) { + files[verseKey].setAttribute('preload', 'auto'); } } } @@ -188,11 +180,11 @@ export class Audioplayer extends Component { handleRepeat = (file) => { const { repeat, - currentAyah, + currentVerse, setRepeat, // eslint-disable-line no-shadow setAyah // eslint-disable-line no-shadow } = this.props; - const [surah, ayah] = currentAyah.split(':').map(val => parseInt(val, 10)); + const [chapter, ayah] = currentVerse.split(':').map(val => parseInt(val, 10)); file.pause(); @@ -235,7 +227,7 @@ export class Audioplayer extends Component { } setRepeat({ ...repeat, times: repeat.times - 1 }); - setAyah(`${surah}:${repeat.from}`); + setAyah(`${chapter}:${repeat.from}`); return this.play(); } @@ -247,21 +239,19 @@ export class Audioplayer extends Component { handleScrollToggle = (event) => { event.preventDefault(); - const { shouldScroll, currentAyah } = this.props; + const { shouldScroll, currentVerse } = this.props; if (!shouldScroll) { // we use the inverse (!) here because we're toggling, so false is true - const elem = document.getElementsByName(`ayah:${currentAyah}`)[0]; - if (elem && elem.getBoundingClientRect().top < 0) { // if the ayah is above our scroll offset - scroller.scrollTo(`ayah:${currentAyah}`, -150); - } else { - scroller.scrollTo(`ayah:${currentAyah}`, -80); - } + this.scrollToVerse(currentVerse); } this.props.toggleScroll(); } handleAddFileListeners(file) { + // NOTE: if no file, just wait. + if (!file) return false; + const { update, currentTime } = this.props; // eslint-disable-line no-shadow debug('component:Audioplayer', `Attaching listeners to ${file.src}`); @@ -353,9 +343,9 @@ export class Audioplayer extends Component { } renderPreviousButton() { - const { currentAyah, files } = this.props; + const { currentVerse, files } = this.props; if (!files) return false; - const index = Object.keys(files).findIndex(id => id === currentAyah); + const index = Object.keys(files).findIndex(id => id === currentVerse); return (
@@ -418,19 +408,19 @@ export class Audioplayer extends Component { return (
- {isLoadedOnClient ? + { + currentFile && : null} + /> + } { - isLoadedOnClient && segments && - segments[currentAyah] && - segments[currentAyah] && + segments[currentVerse] && } @@ -438,9 +428,9 @@ export class Audioplayer extends Component {
- - + + +
); } render() { return (
  • - - - - {' '} - - + {this.renderPopup()}
  • ); } } + +FontSizeDropdown.propTypes = { + onOptionChange: PropTypes.func, + fontSize: customPropTypes.fontSize.isRequired +}; + +export default FontSizeDropdown; diff --git a/src/components/FontSizeDropdown/style.scss b/src/components/FontSizeDropdown/style.scss index 4f19f8725..eedffe1ab 100644 --- a/src/components/FontSizeDropdown/style.scss +++ b/src/components/FontSizeDropdown/style.scss @@ -1,24 +1,20 @@ @import '../../styles/variables.scss'; -.popover{ - :global(.popover-title){ - font-family: $font-montserrat; - text-transform: uppercase; - color: $cream; - padding-top: 15px; - padding-bottom: 15px; - font-size: 0.75em; - } +.list{ + display: table; + width: 100%; + padding: 15px 0px; +} - :global(.popover-content){ - :global(a){ - font-size: 0.8em; - } - } +.item{ + list-style-type: none; + display: table-cell; + width: 33%; } .link{ position: relative; display: block; cursor: pointer; + color: #777; } diff --git a/src/components/FontStyles/index.js b/src/components/FontStyles/index.js index c19539c46..800566818 100644 --- a/src/components/FontStyles/index.js +++ b/src/components/FontStyles/index.js @@ -12,11 +12,8 @@ import selector from './selector'; }), { load } ) -export default class FontStyles extends Component { - static propTypes = { - fontFaces: PropTypes.objectOf(PropTypes.bool).isRequired, - load: PropTypes.func.isRequired - }; + +class FontStyles extends Component { shouldComponentUpdate(nextProps) { return JSON.stringify(this.props.fontFaces) !== JSON.stringify(nextProps.fontFaces); @@ -54,3 +51,10 @@ export default class FontStyles extends Component { ); } } + +FontStyles.propTypes = { + fontFaces: PropTypes.objectOf(PropTypes.bool).isRequired, + load: PropTypes.func.isRequired +}; + +export default FontStyles; diff --git a/src/components/Footer/index.js b/src/components/Footer/index.js index f5e8c5a3c..93e59b5c8 100644 --- a/src/components/Footer/index.js +++ b/src/components/Footer/index.js @@ -1,156 +1,147 @@ import React from 'react'; import Link from 'react-router/lib/Link'; -import Grid from 'react-bootstrap/lib/Grid'; -import Col from 'react-bootstrap/lib/Col'; - import LocaleFormattedMessage from 'components/LocaleFormattedMessage'; const styles = require('./style.scss'); const Footer = () => ( ); diff --git a/src/components/GlobalNav/Surah/index.js b/src/components/GlobalNav/Surah/index.js index e07c4bde5..508c643d9 100644 --- a/src/components/GlobalNav/Surah/index.js +++ b/src/components/GlobalNav/Surah/index.js @@ -1,80 +1,181 @@ -import React, { PropTypes } from 'react'; -import { connect } from 'react-redux'; -import NavDropdown from 'react-bootstrap/lib/NavDropdown'; - -import { surahType, optionsType } from 'types'; +import React, { PropTypes, Component } from 'react'; +import * as customPropTypes from 'customPropTypes'; import * as OptionsActions from 'redux/actions/options.js'; - +import { connect } from 'react-redux'; +import { replace } from 'react-router-redux'; +import Link from 'react-router/lib/Link'; +import Drawer from 'quran-components/lib/Drawer'; +import Menu from 'quran-components/lib/Menu'; +import SearchInput from 'components/SearchInput'; import SurahsDropdown from 'components/SurahsDropdown'; import ReadingModeToggle from 'components/ReadingModeToggle'; import NightModeToggle from 'components/NightModeToggle'; +import InformationToggle from 'components/InformationToggle'; import FontSizeDropdown from 'components/FontSizeDropdown'; +import ReciterDropdown from 'components/ReciterDropdown'; +import ContentDropdown from 'components/ContentDropdown'; +import TooltipDropdown from 'components/TooltipDropdown'; import LocaleFormattedMessage from 'components/LocaleFormattedMessage'; -// TODO: import VersesDropdown from 'components/VersesDropdown'; -import InformationToggle from 'components/InformationToggle'; +import VersesDropdown from 'components/VersesDropdown'; + +import { load, setCurrentVerse } from 'redux/actions/verses.js'; + import GlobalNav from '../index'; const styles = require('../style.scss'); -const GlobalNavSurah = ({ surah, surahs, setOption, options, ...props }) => ( - , - } - > - - - - - - ]} - rightControls={[ - , - , - , - - ]} - /> -); +class GlobalNavSurah extends Component { + state = { + drawerOpen: false + } -GlobalNavSurah.propTypes = { - surah: surahType.isRequired, - surahs: PropTypes.objectOf(surahType).isRequired, - options: optionsType.isRequired, - setOption: PropTypes.func.isRequired, -}; + handleOptionChange = (payload) => { + const { chapter, setOption, options, versesIds } = this.props; + + setOption(payload); + + if (chapter) { + const from = [...versesIds][0]; + const to = [...versesIds][[...versesIds].length - 1]; + const paging = { offset: from - 1, limit: (to - from) + 1 }; + this.props.load(chapter.chapterNumber, paging, { ...options, ...payload }); + } + }; + + handleVerseDropdownClick = (verseNum) => { + const { versesIds, chapter } = this.props; // eslint-disable-line no-shadow + + this.props.setCurrentVerse(`${chapter.chapterNumber}:${verseNum}`); + + if (versesIds.has(verseNum)) { + return false; + } + + return this.props.replace(`/${chapter.chapterNumber}/${verseNum}-${verseNum + 10}`); + } + + handleDrawerToggle = (open) => { + this.setState({ drawerOpen: open }); + } + + renderDrawerToggle(visibleXs) { + return ( +
  • + this.handleDrawerToggle(true)} + > + + + +
  • + ); + } + + render() { + const { chapter, chapters, setOption, versesIds, options, ...props } = this.props; + + return ( + , + , +
    + +
    , +
  • + + + +
  • , + this.renderDrawerToggle(true), + } + > +
    +

    +
    + + + + +
    + + + +
    +
    + +
    + +
    +
    + ]} + rightControls={[ + this.renderDrawerToggle() + ]} + /> + ); + } +} function mapStateToProps(state, ownProps) { - const surahId = parseInt(ownProps.params.surahId, 10); - const surah: Object = state.surahs.entities[surahId]; + const chapterId = parseInt(ownProps.params.chapterId, 10); + const chapter: Object = state.chapters.entities[chapterId]; + const verses: Object = state.verses.entities[chapterId]; + const versesArray = verses ? Object.keys(verses).map(key => parseInt(key.split(':')[1], 10)) : []; + const versesIds = new Set(versesArray); return { - surah, - surahs: state.surahs.entities, - options: state.options + chapter, + chapters: state.chapters.entities, + options: state.options, + versesIds }; } -export default connect(mapStateToProps, OptionsActions)(GlobalNavSurah); +GlobalNavSurah.propTypes = { + chapter: customPropTypes.surahType.isRequired, + chapters: customPropTypes.chapters.isRequired, + options: customPropTypes.optionsType.isRequired, + setOption: PropTypes.func.isRequired, + versesIds: PropTypes.instanceOf(Set), + load: PropTypes.func.isRequired, + setCurrentVerse: PropTypes.func.isRequired, + replace: PropTypes.func.isRequired +}; + +export default connect( + mapStateToProps, + { ...OptionsActions, load, replace, setCurrentVerse } +)(GlobalNavSurah); diff --git a/src/components/GlobalNav/index.js b/src/components/GlobalNav/index.js index 60aeaa33e..7a016927a 100644 --- a/src/components/GlobalNav/index.js +++ b/src/components/GlobalNav/index.js @@ -1,5 +1,6 @@ /* global window */ import React, { PropTypes, Component } from 'react'; +import * as customPropTypes from 'customPropTypes'; import { connect } from 'react-redux'; import Link from 'react-router/lib/Link'; import Navbar from 'react-bootstrap/lib/Navbar'; @@ -8,30 +9,10 @@ import Nav from 'react-bootstrap/lib/Nav'; import LocaleSwitcher from 'components/LocaleSwitcher'; import debug from 'helpers/debug'; -import { userType } from 'types'; const styles = require('./style.scss'); class GlobalNav extends Component { - static propTypes = { - // handleToggleSidebar: PropTypes.func.isRequired, - leftControls: PropTypes.arrayOf(PropTypes.element), - rightControls: PropTypes.arrayOf(PropTypes.element), - handleSidebarToggle: PropTypes.func.isRequired, - isStatic: PropTypes.bool.isRequired, - user: userType, - location: PropTypes.shape({ - action: PropTypes.string, - hash: PropTypes.string, - pathname: PropTypes.string, - search: PropTypes.string, - query: PropTypes.objectOf(PropTypes.string) - }) - }; - - static defaultProps = { - isStatic: false - }; state = { scrolled: false @@ -112,7 +93,7 @@ class GlobalNav extends Component { leftControls.map(((control, index) => React.cloneElement(control, { key: index }))) } -