@@ -10,7 +15,7 @@ const PageBreak = ({ pageNum }) => (
);
PageBreak.propTypes = {
- pageNum: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired
+ pageNum: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
};
export default PageBreak;
diff --git a/src/components/PageBreak/spec.js b/src/components/PageBreak/spec.js
deleted file mode 100644
index d69664e46..000000000
--- a/src/components/PageBreak/spec.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-
-import PageBreak from './index';
-
-let wrapper;
-const pageNum = 15;
-
-describe('
', () => {
- beforeEach(() => {
- wrapper = shallow(
);
- });
-
- it('should render', () => {
- expect(wrapper).to.be.ok; // eslint-disable-line
- });
-
- it('should show page number', () => {
- expect(wrapper.html()).to.contain(`Page ${pageNum}`);
- });
-});
diff --git a/src/components/PageBreak/style.scss b/src/components/PageBreak/style.scss
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/components/PageView/index.js b/src/components/PageView/index.js
deleted file mode 100644
index 64e679c70..000000000
--- a/src/components/PageView/index.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import React, { PropTypes } from 'react';
-import * as customPropTypes from 'customPropTypes';
-import { connect } from 'react-redux';
-
-import Line from 'components/Line';
-import PageBreak from 'components/PageBreak';
-
-const PageView = ({ lines, keys, currentVerse, options, isPlaying, audioActions, userAgent }) => { // eslint-disable-line
- const elements = keys.map((lineNum, index) => {
- const nextNum = keys[index + 1];
- const pageNum = lineNum.split('-')[0];
- const line = lines[lineNum];
- const renderText = false; // userAgent.isBot;
-
- if (index + 1 !== keys.length && pageNum !== nextNum.split('-')[0]) {
- return [
-
,
-
- ];
- }
-
- return (
-
- );
- });
-
- return (
-
{elements}
- );
-};
-
-PageView.propTypes = {
- keys: PropTypes.array, // eslint-disable-line
- lines: PropTypes.object.isRequired, // eslint-disable-line
- audioActions: customPropTypes.audioActions.isRequired, // eslint-disable-line
- currentVerse: PropTypes.string,
- bookmarks: PropTypes.object.isRequired, // eslint-disable-line
- options: PropTypes.object.isRequired, // eslint-disable-line
- isPlaying: PropTypes.bool,
- userAgent: PropTypes.func
-};
-
-export default connect(state => ({
- userAgent: state.options.userAgent
-}))(PageView);
diff --git a/src/components/QuickChapters.tsx b/src/components/QuickChapters.tsx
new file mode 100644
index 000000000..84e99f79a
--- /dev/null
+++ b/src/components/QuickChapters.tsx
@@ -0,0 +1,80 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import styled from 'styled-components';
+
+import T, { KEYS } from './T';
+import Title from './home/Title';
+
+import { QUICK_LINKS_EVENTS } from '../events';
+
+const Span = styled.span`
+ &:after {
+ content: '|';
+ }
+ &:first-child,
+ &:last-child {
+ &:after {
+ content: none;
+ }
+ }
+`;
+
+const isFriday = new Date().getDay() === 5;
+
+const QuickChapters: React.SFC<{}> = () => (
+
+
+
+ {__CLIENT__ &&
+ isFriday && (
+
+
+ Surah Al-Kahf
+
+
+ )}
+
+
+ Surah Yasin (Yaseen)
+
+
+
+
+ Surah Ar-Rahman
+
+
+
+
+ Surah Al Mulk
+
+
+
+
+ Ayatul Kursi
+
+
+
+
+);
+
+export default QuickChapters;
diff --git a/src/components/ReadingModeToggle/index.js b/src/components/ReadingModeToggle/index.js
deleted file mode 100644
index 76146bdaa..000000000
--- a/src/components/ReadingModeToggle/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import React, { PropTypes } from 'react';
-import LocaleFormattedMessage from 'components/LocaleFormattedMessage';
-import { MenuItem } from 'quran-components/lib/Menu';
-
-const ReadingModeToggle = ({ onToggle, isToggled }) => (
-
}
- onClick={() => onToggle({ isReadingMode: !isToggled })}
- >
-
-
-);
-
-ReadingModeToggle.propTypes = {
- onToggle: PropTypes.func.isRequired,
- isToggled: PropTypes.bool.isRequired
-};
-
-export default ReadingModeToggle;
diff --git a/src/components/ReciterDropdown/index.js b/src/components/ReciterDropdown/index.js
deleted file mode 100644
index b6e407407..000000000
--- a/src/components/ReciterDropdown/index.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import React, { Component, PropTypes } from 'react';
-import * as customPropTypes from 'customPropTypes';
-import { connect } from 'react-redux';
-import Menu, { MenuItem } from 'quran-components/lib/Menu';
-import Radio from 'quran-components/lib/Radio';
-import Loader from 'quran-components/lib/Loader';
-import Icon from 'quran-components/lib/Icon';
-import LocaleFormattedMessage from 'components/LocaleFormattedMessage';
-import { loadRecitations } from 'redux/actions/options';
-
-class ReciterDropdown extends Component {
-
- componentDidMount() {
- if (!this.props.recitations.length) {
- return this.props.loadRecitations();
- }
-
- return false;
- }
-
- renderMenu() {
- const { audio, onOptionChange, recitations } = this.props;
-
- return recitations.map(slug => (
-
- onOptionChange({ audio: slug.id })}
- >
-
- {slug.reciterNameEng} {slug.style ? `(${slug.style})` : ''}
-
-
-
- ));
- }
-
- render() {
- const { recitations } = this.props;
-
- return (
-
}
- menu={
- recitations.length ?
{this.renderMenu()} :
- }
- >
-
-
- );
- }
-}
-
-ReciterDropdown.propTypes = {
- onOptionChange: PropTypes.func,
- audio: PropTypes.number,
- loadRecitations: PropTypes.func.isRequired,
- recitations: customPropTypes.recitations
-};
-
-export default connect(state => ({
- recitations: state.options.options.recitations,
- loadingRecitations: state.options.loadingRecitations,
- audio: state.options.audio
-}), { loadRecitations })(ReciterDropdown);
diff --git a/src/components/ReciterDropdown/style.scss b/src/components/ReciterDropdown/style.scss
deleted file mode 100644
index ae987cee9..000000000
--- a/src/components/ReciterDropdown/style.scss
+++ /dev/null
@@ -1,13 +0,0 @@
-:local .dropdown{
- @media (max-width: 1150px) {
- :global(i + span){
- display: none;
- }
- }
-
- @media (max-width: 1000px) {
- :global(i + span){
- display: initial;
- }
- }
-}
diff --git a/src/components/RouterStatus.tsx b/src/components/RouterStatus.tsx
new file mode 100644
index 000000000..416320c78
--- /dev/null
+++ b/src/components/RouterStatus.tsx
@@ -0,0 +1,30 @@
+import React, { ReactNode } from 'react';
+import PropTypes from 'prop-types';
+import { Route } from 'react-router';
+
+const propTypes = {
+ code: PropTypes.number.isRequired,
+ children: PropTypes.node.isRequired,
+};
+
+type Props = {
+ code: number;
+ children: ReactNode;
+};
+
+const Status: React.SFC
= ({ code, children }: Props) => (
+ {
+ if (staticContext) {
+ // eslint-disable-next-line
+ staticContext.status = code;
+ }
+
+ return children;
+ }}
+ />
+);
+
+Status.propTypes = propTypes;
+
+export default Status;
diff --git a/src/components/Routes.tsx b/src/components/Routes.tsx
new file mode 100644
index 000000000..20d304275
--- /dev/null
+++ b/src/components/Routes.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { Switch, Route } from 'react-router';
+
+import routes from '../routes';
+import NotFound from './NotFound';
+
+const defaultSetContext = (context: any) => ({
+ ...context,
+ status: 200,
+});
+
+const Routes: React.SFC = () => (
+
+ {routes.map(({ component: Component, setContext, ...route }: $TsFixMe) => (
+ {
+ if (staticContext) {
+ const contextFunction = setContext || defaultSetContext;
+
+ Object.assign(staticContext, contextFunction(staticContext));
+ }
+
+ return ;
+ }}
+ />
+ ))}
+
+
+);
+
+export default Routes;
diff --git a/src/components/Search.tsx b/src/components/Search.tsx
new file mode 100644
index 000000000..2b5184b9a
--- /dev/null
+++ b/src/components/Search.tsx
@@ -0,0 +1,275 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import qs from 'qs';
+import { PropTypes as MetricsPropTypes } from 'react-metrics';
+import Helmet from 'react-helmet';
+import ReactPaginate from 'react-paginate';
+import Loader from 'quran-components/lib/Loader';
+import { FormattedHTMLMessage } from 'react-intl';
+import { History, Location } from 'history';
+import Jumbotron from './Jumbotron';
+import VerseContainer from '../containers/VerseContainer';
+import T, { KEYS } from './T';
+import { VerseShape } from '../shapes';
+import SettingsShape from '../shapes/SettingsShape';
+import { FetchSearch } from '../redux/actions/search';
+
+const Header = styled.div`
+ background-color: #e7e6e6;
+ min-height: 50px;
+ padding: 15px 0;
+ color: #414141;
+ font-weight: 400;
+
+ .pagination {
+ margin: 0;
+
+ & > li:first-child > a,
+ & > li:last-child > a {
+ font-size: 14px;
+
+ &.disabled {
+ opacity: 0.5;
+ }
+ }
+
+ & > li {
+ &.active {
+ a {
+ color: $brand-primary;
+ }
+ }
+
+ &.disabled {
+ opacity: 0.5;
+ }
+ }
+
+ & > li > a {
+ background: transparent;
+ border: none;
+ color: #414141;
+ float: initial;
+ padding: 6px 18px;
+ font-weight: 300;
+ font-size: 14px;
+
+ &:hover,
+ &:focus {
+ background: initial;
+ }
+
+ i {
+ font-size: 12px;
+ }
+ }
+
+ .selected a {
+ color: ${({ theme }) => theme.brandPrimary};
+ }
+
+ @media (max-width: ${({ theme }) => theme.breakpoints.sm}) {
+ padding-top: 5px;
+ }
+ }
+`;
+
+const propTypes = {
+ isErrored: PropTypes.bool.isRequired,
+ isLoading: PropTypes.bool.isRequired,
+ totalCount: PropTypes.number.isRequired,
+ totalPages: PropTypes.number.isRequired,
+ currentPage: PropTypes.number.isRequired,
+ perPage: PropTypes.number.isRequired,
+ query: PropTypes.string.isRequired,
+ entities: PropTypes.arrayOf(VerseShape),
+ history: PropTypes.object.isRequired,
+ location: PropTypes.shape({
+ search: PropTypes.string.isRequired,
+ }).isRequired,
+ settings: SettingsShape.isRequired,
+ fetchSearch: PropTypes.func.isRequired,
+};
+
+const defaultProps: { entities: Array } = {
+ entities: [],
+};
+
+const contextTypes = {
+ metrics: MetricsPropTypes.metrics,
+};
+
+type Props = {
+ isErrored: boolean;
+ isLoading: boolean;
+ totalCount: number;
+ totalPages: number;
+ currentPage: number;
+ perPage: number;
+ query: string;
+ entities: Array;
+ location: Location;
+ settings: SettingsShape;
+ fetchSearch: FetchSearch;
+ history: History;
+};
+
+class Search extends Component {
+ static propTypes = propTypes;
+
+ static defaultProps = defaultProps;
+
+ static contextTypes = contextTypes;
+
+ bootstrap() {
+ const { fetchSearch, location } = this.props;
+
+ const query = qs.parse(location.search);
+
+ return fetchSearch(query.query || query.q);
+ }
+
+ handlePageChange = (payload: $TsFixMe) => {
+ const {
+ history: { push },
+ query,
+ currentPage,
+ } = this.props;
+ const { metrics } = this.context;
+
+ const selectedPage = payload.selected + 1;
+
+ if (currentPage !== selectedPage) {
+ metrics.track('Search', {
+ action: 'paginate',
+ label: `${query} - ${selectedPage}`,
+ });
+
+ return push({
+ pathname: '/search',
+ search: '{ p: selectedPage, q: query }',
+ });
+ }
+
+ return true;
+ };
+
+ renderStatsBar() {
+ const { totalCount, totalPages, currentPage, query, perPage } = this.props;
+ const from = Math.max(...[(currentPage - 1) * perPage, 1]);
+ const to = Math.min(...[currentPage * perPage, totalCount]);
+ const values = { from, to, query, total: totalCount };
+
+ if (totalPages) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ }
+ nextLabel={
+
+
+
+ }
+ breakLabel={... }
+ marginPagesDisplayed={2}
+ pageRangeDisplayed={5}
+ onPageChange={this.handlePageChange}
+ containerClassName="pagination"
+ pageLinkClassName="pointer"
+ pageCount={totalPages}
+ />
+
+
+
+
+ );
+ }
+
+ return false;
+ }
+
+ renderBody() {
+ const {
+ isErrored,
+ isLoading,
+ entities,
+ location: { search },
+ } = this.props;
+
+ const query = qs.parse(search);
+
+ if (!query || !query.q) {
+ return (
+
+
+
+ );
+ }
+
+ if (isErrored) {
+ return (
+
+
+
+ );
+ }
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (!entities.length) {
+ return (
+
+
+
+ );
+ }
+
+ return entities.map(verse => (
+
+ ));
+ }
+
+ render() {
+ const { query, settings } = this.props;
+
+ return (
+
+
+
+ {this.renderStatsBar()}
+
+
+ );
+ }
+}
+
+export default Search;
diff --git a/src/components/SearchAutocomplete.tsx b/src/components/SearchAutocomplete.tsx
new file mode 100644
index 000000000..d9211aade
--- /dev/null
+++ b/src/components/SearchAutocomplete.tsx
@@ -0,0 +1,252 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import debounce from 'lodash/debounce';
+import { Link } from 'react-router-dom';
+import { ChapterShape, SuggestionShape } from '../shapes';
+import { FetchSuggest } from '../redux/actions/suggest';
+
+const verseRegex = /^(\d+)(?::(\d+))?$/;
+
+const Container = styled.div`
+ width: 100%;
+ background-color: #ccc;
+ position: absolute;
+ z-index: 99;
+`;
+
+const List = styled.ul<{ textColor?: string }>`
+ left: 0;
+ z-index: 1;
+ min-width: 100%;
+ box-sizing: border-box;
+ list-style: none;
+ padding: 0;
+ margin: 0.2em 0 0;
+ background: white;
+ border: 1px solid rgba(0, 0, 0, 0.3);
+ box-shadow: 0.05em 0.2em 0.6em rgba(0, 0, 0, 0.2);
+ text-shadow: none;
+
+ &:before {
+ content: '';
+ position: absolute;
+ top: -0.23em;
+ left: 1em;
+ width: 0;
+ height: 0;
+ padding: 0.4em;
+ background: white;
+ border: inherit;
+ border-right: 0;
+ border-bottom: 0;
+ -webkit-transform: rotate(45deg);
+ transform: rotate(45deg);
+ }
+
+ & > li {
+ position: relative;
+ cursor: pointer;
+ text-align: left;
+ }
+
+ .text {
+ overflow: hidden;
+ word-wrap: break-word;
+ white-space: nowrap;
+ line-height: 28px;
+ padding-left: 10px;
+ padding-top: 0.2em;
+ padding-bottom: 0.2em;
+ a {
+ display: block;
+ }
+ }
+
+ & > li:hover .text,
+ & > li:focus .text {
+ background-color: rgba(${({ theme }) => theme.brandPrimary}, 0.5);
+ color: ${({ textColor }) => textColor};
+ }
+
+ & > li:hover .text a,
+ & > li:focus .text a {
+ color: #444;
+ }
+
+ & > li:hover .link a,
+ & > li:focus .link a {
+ color: #444;
+ }
+
+ & > li[aria-selected='true'] .text {
+ background: hsl(205, 40%, 40%);
+ color: white;
+ }
+
+ & li:hover mark,
+ & li:focus mark {
+ background: hsl(68, 100%, 41%);
+ }
+
+ li[aria-selected='true'] mark {
+ background: hsl(86, 100%, 21%);
+ color: inherit;
+ }
+
+ mark {
+ background: hsl(65, 100%, 50%);
+ }
+`;
+
+const StyledLink = styled.div`
+ position: absolute;
+ right: 0;
+ padding-top: 0.2em;
+ padding-bottom: 0.2em;
+ padding-left: 70px;
+ padding-right: 10px;
+ line-height: 28px;
+ background: linear-gradient(
+ to right,
+ rgba(255, 255, 255, 0),
+ white 40%,
+ rgba(255, 255, 255, 1)
+ );
+ z-index: 2;
+ text-align: right;
+`;
+
+const propTypes = {
+ fetchSuggest: PropTypes.func.isRequired,
+ chapters: PropTypes.shape({
+ chapterId: ChapterShape,
+ }),
+ value: PropTypes.string,
+ suggestions: PropTypes.arrayOf(SuggestionShape),
+ lang: PropTypes.string,
+};
+
+const defaultProps = {
+ lang: '',
+ value: '',
+ chapters: {},
+ suggestions: [],
+};
+
+type Props = {
+ fetchSuggest: FetchSuggest;
+ chapters?: { [chapterId: string]: ChapterShape };
+ value?: string;
+ suggestions?: Array;
+ lang?: string;
+};
+
+class SearchAutocomplete extends Component {
+ static propTypes = propTypes;
+ static defaultProps = defaultProps;
+
+ componentDidUpdate(prevProps: Props) {
+ const { value } = this.props;
+
+ if (value !== prevProps.value) {
+ return this.fetchSuggest();
+ }
+
+ return null;
+ }
+
+ fetchSuggest = debounce(() => {
+ const { fetchSuggest, value, lang } = this.props;
+
+ if (!value || verseRegex.test(value)) return null;
+
+ return fetchSuggest(value, lang);
+ }, 500);
+
+ getSuggestions() {
+ const { value, suggestions } = this.props;
+
+ const chapterSuggestions = this.getChapterSuggestions(value);
+
+ if (chapterSuggestions) {
+ return [...suggestions, ...chapterSuggestions];
+ }
+
+ return suggestions;
+ }
+
+ getChapterSuggestions = (value: string) => {
+ const { chapters } = this.props;
+
+ const matches: $TsFixMe = [];
+
+ if (!value) return matches;
+
+ const isVerseKeySearch = verseRegex.test(value);
+
+ if (isVerseKeySearch) {
+ const captures = value.match(verseRegex);
+
+ if (captures) {
+ const chapterId = captures[1];
+ const ayahNum = captures[2];
+ const chapter = chapters[chapterId];
+
+ matches.push([
+ chapter.nameSimple,
+ chapter.chapterNumber + (ayahNum ? `/${ayahNum}` : ''),
+ ]);
+ }
+ } else if (value.length >= 2) {
+ const escaped = value.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&');
+
+ Object.keys(chapters).forEach(chapterId => {
+ const chapter = chapters[chapterId];
+
+ if (
+ RegExp(escaped, 'i').test(chapter.nameSimple.replace(/['-]/g, ''))
+ ) {
+ matches.push([chapter.nameSimple, chapter.chapterNumber]);
+ } else if (RegExp(escaped, 'i').test(chapter.nameArabic)) {
+ matches.push([chapter.nameArabic, chapter.chapterNumber]);
+ }
+ });
+ }
+
+ return matches
+ .map((match: Array) => ({
+ text: `${match[0]} `,
+ href: `/${match[1]}`,
+ }))
+ .slice(0, 5);
+ };
+
+ render() {
+ const suggestions = this.getSuggestions();
+ const hasSuggestions = !!suggestions.length;
+
+ return (
+
+
+ {hasSuggestions &&
+ suggestions.map((item: SuggestionShape) => (
+
+
+ {item.ayah}
+
+
+
+
+
+ ))}
+
+
+ );
+ }
+}
+
+export default SearchAutocomplete;
diff --git a/src/components/SearchAutocomplete/index.js b/src/components/SearchAutocomplete/index.js
deleted file mode 100644
index 1392cf8b1..000000000
--- a/src/components/SearchAutocomplete/index.js
+++ /dev/null
@@ -1,219 +0,0 @@
-// TODO: Should be handled by redux and not component states.
-import React, { Component, PropTypes } from 'react';
-import * as customPropTypes from 'customPropTypes';
-import { connect } from 'react-redux';
-import { push } from 'react-router-redux';
-import { suggest } from 'redux/actions/suggest';
-
-const styles = require('./style.scss');
-
-const ayahRegex = /^(\d+)(?::(\d+))?$/;
-
-class SearchAutocomplete extends Component {
-
- componentDidMount() {
- this.props.input.addEventListener('keydown', this.handleInputKeyDown.bind(this));
- }
-
- componentWillReceiveProps(nextProps) {
- if (this.props.value !== nextProps.value) {
- if (this.timer) {
- clearTimeout(this.timer);
- }
-
- this.timer = setTimeout(() => {
- this.suggest(nextProps.value);
- }, this.props.delay);
- }
-
- return false;
- }
-
- getSuggestions() {
- let suggestions = this.getSurahSuggestions(this.props.value);
-
- if (this.props.suggestions) {
- suggestions = suggestions.concat(this.props.suggestions);
- }
-
- return suggestions;
- }
-
- getSurahSuggestions = (value) => {
- const matches = [];
-
- if (!value) return matches;
-
- const isverseKeySearch = ayahRegex.test(value);
-
- if (isverseKeySearch) {
- const captures = value.match(ayahRegex);
- const chapterId = captures[1];
- const ayahNum = captures[2];
- const chapter = this.props.chapters[chapterId];
- matches.push([chapter.nameSimple, chapter.chapterNumber + (ayahNum ? `/${ayahNum}` : '')]);
- } else if (value.length >= 2) {
- const escaped = value.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&');
-
- Object.keys(this.props.chapters).forEach((chapterId) => {
- const chapter = this.props.chapters[chapterId];
- if (RegExp(escaped, 'i').test(chapter.nameSimple.replace(/['-]/g, ''))) {
- matches.push([chapter.nameSimple, chapter.chapterNumber]);
- } else if (RegExp(escaped, 'i').test(chapter.nameArabic)) {
- matches.push([chapter.nameArabic, chapter.chapterNumber]);
- }
- });
- }
-
- return matches.map(match => ({
- text: `${match[0]} `,
- href: `/${match[1]}`
- })).slice(0, 5);
- }
-
- suggest = (query) => {
- const { lang } = this.props;
-
- if (!query || ayahRegex.test(query)) return false;
-
- return this.props.suggest(query, lang);
- }
-
- handleInputKeyDown = (event) => {
- if (!(event.keyCode === 9 || event.keyCode === 40 || event.keyCode === 27)) {
- return;
- }
-
- const items = this.menu.getElementsByTagName('li');
-
- if (!items.length) {
- return;
- }
-
- switch (event.keyCode) {
- case 9: // tab
- items[0].focus();
- break;
- case 27: // escape
- // TODO if open closeMenu()
- break;
- case 40: // down
- items[0].focus();
- break;
- default:
- return;
- }
- event.preventDefault();
- }
-
- handleItemKeyDown(event, item) {
- const items = this.menu.getElementsByTagName('li');
-
- if (!items.length) {
- return;
- }
-
- switch (event.keyCode) {
- case 9: // tab
- return;
- case 13: // enter
- this.props.push(item.href); // change url
- break;
- case 27: // escape
- // TODO if open closeMenu()
- break;
- case 38: // up
- if (event.target === items[0]) { // we're on the first item, so focus the input
- this.props.input.focus();
- } else {
- event.target.previousSibling.focus();
- }
- break;
- case 40: // down
- if (event.target === items[items.length - 1]) {
- items[0].focus();
- } else {
- event.target.nextSibling.focus();
- }
- break;
- default:
- return;
- }
- event.preventDefault();
- }
-
- renderList() {
- if (!this.getSuggestions().length) {
- return false;
- }
-
- return this.getSuggestions().map(item => (
- this.handleItemKeyDown(event, item)}
- >
-
-
-
- ));
- }
-
- render() {
- return (
-
-
{ this.menu = ref; }}>
- {this.renderList()}
-
-
- );
- }
-}
-
-function mapStateToProps(state, ownProps) {
- const chapters = state.chapters.entities;
- const chapterId = state.chapters.current;
- const suggestions = state.suggestResults.results[ownProps.value];
- let lang = 'en';
-
- if (state.verses && state.verses.entities && state.verses.entities[chapterId]) {
- const ayahs = state.verses.entities[chapterId];
- const verseKey = Object.keys(ayahs)[0];
-
- if (verseKey) {
- const ayah = ayahs[verseKey];
-
- if (ayah.content && ayah.content[0] && ayah.content[0].lang) {
- lang = ayah.content[0].lang;
- }
- }
- }
-
- return {
- chapters,
- suggestions,
- lang
- };
-}
-
-SearchAutocomplete.propTypes = {
- chapters: customPropTypes.chapters.isRequired,
- value: PropTypes.string,
- // TODO: This should not be doing html stuff. Should use react onKeydown.
- input: PropTypes.any, // eslint-disable-line
- push: PropTypes.func.isRequired,
- suggest: PropTypes.func.isRequired,
- suggestions: customPropTypes.suggestions,
- lang: PropTypes.string,
- delay: PropTypes.number,
-};
-
-SearchAutocomplete.defaultProps = {
- delay: 200
-};
-
-export default connect(mapStateToProps, { push, suggest })(SearchAutocomplete);
diff --git a/src/components/SearchAutocomplete/style.scss b/src/components/SearchAutocomplete/style.scss
deleted file mode 100644
index 06ca208ee..000000000
--- a/src/components/SearchAutocomplete/style.scss
+++ /dev/null
@@ -1,92 +0,0 @@
-@import '../../styles/variables.scss';
-
-.autocomplete {
- width: 100%;
- background-color: #ccc;
- position: absolute;
- z-index: 99;
-
- .list{
- left: 0;
- z-index: 1;
- min-width: 100%;
- box-sizing: border-box;
- list-style: none;
- padding: 0;
- margin: .2em 0 0;
- background: white;
- border: 1px solid rgba(0,0,0,.3);
- box-shadow: .05em .2em .6em rgba(0,0,0,.2);
- text-shadow: none;
- }
-
- /* Pointer */
- .list:before {
- content: "";
- position: absolute;
- top: -.23em;
- left: 1em;
- width: 0; height: 0;
- padding: .4em;
- background: white;
- border: inherit;
- border-right: 0;
- border-bottom: 0;
- -webkit-transform: rotate(45deg);
- transform: rotate(45deg);
- }
- .list > li {
- position: relative;
- cursor: pointer;
- text-align: left;
- }
-
- .link {
- position: absolute;
- right: 0;
- padding-top: .2em;
- padding-bottom: .2em;
- padding-left: 70px;
- padding-right: 10px;
- line-height: 28px;
- background: linear-gradient(to right,rgba(255, 255, 255, 0), white 40%, rgba(255,255,255,1));
- z-index: 2;
- text-align: right;
- }
- .text {
- overflow: hidden;
- word-wrap: break-word;
- white-space: nowrap;
- line-height: 28px;
- padding-left: 10px;
- padding-top: .2em;
- padding-bottom: .2em;
- a {
- display: block;
- }
- }
- .list > li:hover .text, .list > li:focus .text {
- background-color: rgba($brand-primary, 0.5);
- color: $text-color;
- }
- .list > li:hover .text a, .list > li:focus .text a {
- color: #444;
- }
- .list > li:hover .link a, .list > li:focus .link a {
- color: #444;
- }
- .list > li[aria-selected="true"] .text {
- background: hsl(205, 40%, 40%);
- color: white;
- }
- mark {
- background: hsl(65, 100%, 50%);
- }
- .list li:hover mark, .list li:focus mark {
- background: hsl(68, 100%, 41%);
- }
- li[aria-selected="true"] mark {
- background: hsl(86, 100%, 21%);
- color: inherit;
- }
-}
diff --git a/src/components/SearchInput.tsx b/src/components/SearchInput.tsx
new file mode 100644
index 000000000..5ee37ec85
--- /dev/null
+++ b/src/components/SearchInput.tsx
@@ -0,0 +1,193 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import { lighten } from 'polished';
+import toNumber from 'lodash/toNumber';
+import OutsideClickHandler from 'react-outside-click-handler';
+import { withRouter } from 'react-router-dom';
+import { RouteComponentProps } from 'react-router';
+import { PropTypes as MetricsPropTypes } from 'react-metrics';
+import { History } from 'history';
+
+import T, { KEYS } from './T';
+import SearchAutocompleteContainer from '../containers/SearchAutocompleteContainer';
+
+const arabic = new RegExp(/[\u0600-\u06FF]/);
+// eslint-disable-next-line
+const shortcutSearch = /\d[\.,\:,\,,\\,//]/g;
+// eslint-disable-next-line
+const splitSearch = /[\.,\:,\,,\\,//]/g;
+
+const Input = styled.input<{ isArabic?: boolean }>`
+ text-align: ${({ isArabic }) => (isArabic ? 'right' : 'left')};
+ width: 100%;
+ padding: 20px 30px;
+ padding-right: 60px;
+ outline: none;
+ border: none;
+ background-color: #fff;
+ color: ${({ theme }) => theme.brandPrimary};
+
+ &::-webkit-input-placeholder {
+ color: ${({ theme }) => theme.brandPrimary};
+ font-weight: 300;
+ }
+
+ &:-moz-placeholder {
+ /* Firefox 18- */
+ color: ${({ theme }) => theme.brandPrimary};
+ font-weight: 300;
+ }
+
+ &::-moz-placeholder {
+ /* Firefox 19+ */
+ color: ${({ theme }) => theme.brandPrimary};
+ font-weight: 300;
+ }
+
+ &:-ms-input-placeholder {
+ color: ${({ theme }) => theme.brandPrimary};
+ font-weight: 300;
+ }
+`;
+
+const Button = styled.button`
+ position: absolute;
+ right: 0;
+ padding: 15px 15px;
+ font-size: 150%;
+ background-color: ${({ theme }) => theme.brandPrimary};
+ color: #fff;
+ cursor: pointer;
+ height: 100%;
+ border: 3px solid #fff;
+
+ &:hover {
+ background-color: ${({ theme }) => lighten(0.1, theme.brandPrimary)};
+ }
+`;
+
+const contextTypes = {
+ metrics: MetricsPropTypes.metrics,
+};
+
+const propTypes = {
+ className: PropTypes.string,
+ history: PropTypes.object.isRequired,
+};
+
+const defaultProps = {
+ className: '',
+};
+
+type Props = {
+ history: History;
+ className?: string;
+} & RouteComponentProps<{}>;
+
+type State = {
+ value: string;
+ showAutocomplete: boolean;
+};
+
+class SearchInput extends Component {
+ static propTypes = propTypes;
+ static defaultProps = defaultProps;
+ static contextTypes = contextTypes;
+
+ readonly state: State = {
+ value: '',
+ showAutocomplete: false,
+ };
+
+ handleSearch = (value: string) => {
+ const {
+ history: { push },
+ } = this.props;
+ const { metrics } = this.context;
+
+ let verse;
+ let chapter;
+ const pattern = new RegExp(shortcutSearch);
+
+ if (pattern.test(value)) {
+ chapter = toNumber(value.split(splitSearch)[0]);
+ verse = toNumber(value.split(splitSearch)[1]);
+
+ if (isNaN(verse)) {
+ verse = 1;
+ }
+
+ metrics.track('Search', {
+ action: 'chapter',
+ label: `/${chapter}/${verse}-${verse + 10}`,
+ });
+
+ return push(`/${chapter}/${verse}-${verse + 10}`);
+ }
+
+ metrics.track('Search', {
+ action: 'query',
+ label: value,
+ });
+
+ this.handleHideAutocomplete();
+
+ return push(`/search?q=${value}`);
+ };
+
+ handleKeyUp = (event: $TsFixMe) => {
+ if (
+ event.key === 'Enter' ||
+ event.keyCode === 13 ||
+ event.type === 'click'
+ ) {
+ const value = event.target.value.trim();
+
+ this.handleSearch(value);
+ }
+
+ this.setState({ value: event.target.value.trim() });
+
+ return null;
+ };
+
+ handleButtonClick = () => {
+ const { value } = this.state;
+
+ this.handleSearch(value);
+ };
+
+ handleInputFocus = () => this.setState({ showAutocomplete: true });
+
+ handleHideAutocomplete = () => this.setState({ showAutocomplete: false });
+
+ render() {
+ const { showAutocomplete, value } = this.state;
+ const { className } = this.props;
+
+ return (
+
+
+
+
+
+
+ {placeholder => (
+
+ )}
+
+ {showAutocomplete && }
+
+
+ );
+ }
+}
+
+export default withRouter(SearchInput);
diff --git a/src/components/SearchInput/index.js b/src/components/SearchInput/index.js
deleted file mode 100644
index df9885d4a..000000000
--- a/src/components/SearchInput/index.js
+++ /dev/null
@@ -1,120 +0,0 @@
-import React, { Component, PropTypes } from 'react';
-import { PropTypes as MetricsPropTypes } from 'react-metrics';
-import { push } from 'react-router-redux';
-import { connect } from 'react-redux';
-import { intlShape, injectIntl } from 'react-intl';
-
-import SearchAutocomplete from 'components/SearchAutocomplete';
-
-import debug from 'helpers/debug';
-
-class SearchInput extends Component {
-
- static contextTypes = {
- metrics: MetricsPropTypes.metrics
- };
-
- state = {
- value: '',
- showAutocomplete: false
- };
-
- search = (event) => {
- const arabic = new RegExp(/[\u0600-\u06FF]/);
- const shortcutSearch = /\d[\.,\:,\,,\\,//]/g; // eslint-disable-line no-useless-escape
- const splitSearch = /[\.,\:,\,,\\,//]/g; // eslint-disable-line no-useless-escape
-
- if (event.key === 'Enter' || event.keyCode === 13 || event.type === 'click') {
- const inputEl = this.input;
- const searching = inputEl.value.trim();
- let ayah;
- let surah;
-
- // prevent search function while search input field is empty
- if (searching === '') {
- // reset input to display "Search" placeholder text
- inputEl.value = '';
- return false;
- }
-
- const pattern = new RegExp(shortcutSearch);
-
- if (pattern.test(searching)) {
- surah = parseInt(
- searching.split(splitSearch)[0],
- 10
- );
- ayah = parseInt(
- searching.split(splitSearch)[1],
- 10
- );
-
- if (isNaN(ayah)) {
- ayah = 1;
- }
-
- this.context.metrics.track('Search', {
- action: 'surah',
- label: `/${surah}/${ayah}-${(ayah + 10)}`
- });
-
- return this.props.push(`/${surah}/${ayah}-${(ayah + 10)}`);
- }
-
- this.context.metrics.track('Search', {
- action: 'query',
- label: searching
- });
-
- return this.props.push(`/search?q=${searching}`);
- }
-
- // This checks to see if the user is typing Arabic
- // and adjusts the text-align.
- if (arabic.test(event.target.value)) {
- event.target.style.textAlign = 'right'; // eslint-disable-line no-param-reassign
- } else {
- event.target.style.textAlign = 'left'; // eslint-disable-line no-param-reassign
- }
-
- if (this.input) {
- this.setState({ value: this.input.value.trim() });
- }
-
- return false;
- }
-
- render() {
- const { showAutocomplete } = this.state;
- const { className, intl } = this.props;
- const placeholder = intl.formatMessage({ id: 'search.placeholder', defaultMessage: 'Search' });
-
- debug('component:SearchInput', 'Render');
-
- return (
-
-
-
{ this.input = input; }}
- onFocus={() => this.setState({ showAutocomplete: true })}
- // onBlur={() => this.setState({ showAutocomplete: false })}
- onKeyUp={this.search}
- />
- {
- showAutocomplete &&
-
- }
-
- );
- }
-}
-
-SearchInput.propTypes = {
- push: PropTypes.func.isRequired,
- className: PropTypes.string,
- intl: intlShape.isRequired
-};
-
-export default injectIntl(connect(null, { push })(SearchInput));
diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx
new file mode 100644
index 000000000..45850a7ab
--- /dev/null
+++ b/src/components/Settings.tsx
@@ -0,0 +1,91 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import last from 'lodash/last';
+import Menu from 'quran-components/lib/Menu';
+import LocaleSwitcher from './LocaleSwitcher';
+import FontSizeOptions from './settings/FontSizeOptions';
+import ReadingModeToggle from './settings/ReadingModeToggle';
+import NightModeToggle from './settings/NightModeToggle';
+import ChapterInfoToggle from './settings/ChapterInfoToggle';
+import ReciterDropdownContainer from '../containers/settings/ReciterDropdownContainer';
+import TranslationsDropdownContainer from '../containers/settings/TranslationsDropdownContainer';
+import TooltipOptions from './settings/TooltipOptions';
+import { VerseShape, ChapterShape, SettingsShape } from '../shapes';
+import { SetSetting } from '../redux/actions/settings';
+import { FetchVerses } from '../redux/actions/verses';
+
+const propTypes = {
+ setSetting: PropTypes.func.isRequired,
+ fetchVerses: PropTypes.func.isRequired,
+ chapter: ChapterShape.isRequired,
+ settings: SettingsShape.isRequired,
+ verses: PropTypes.shape({
+ verseKey: VerseShape,
+ }).isRequired,
+};
+
+type Props = {
+ setSetting: SetSetting;
+ fetchVerses: FetchVerses;
+ chapter: ChapterShape;
+ settings: SettingsShape;
+ verses: { [verseKey: string]: VerseShape };
+};
+
+class Settings extends Component {
+ static propTypes = propTypes;
+
+ handleSettingChange = (payload: $TsFixMe) => {
+ const { chapter, setSetting, settings, fetchVerses, verses } = this.props;
+
+ setSetting(payload);
+
+ if (chapter) {
+ const from = Object.values(verses)[0].verseNumber;
+ const lastVerse: VerseShape | undefined = last(Object.values(verses));
+ const to = lastVerse && lastVerse.verseNumber;
+ const paging = { offset: from - 1, limit: to ? to - from + 1 : 10 };
+
+ fetchVerses(
+ chapter.chapterNumber,
+ paging,
+ {
+ ...settings,
+ ...payload,
+ },
+ settings
+ );
+ }
+ };
+
+ render() {
+ const { setSetting, settings } = this.props;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default Settings;
diff --git a/src/components/SettingsModal/index.js b/src/components/SettingsModal/index.js
deleted file mode 100644
index 2b8114410..000000000
--- a/src/components/SettingsModal/index.js
+++ /dev/null
@@ -1,89 +0,0 @@
-import React, { PropTypes } from 'react';
-import * as customProptypes from 'customPropTypes';
-import { connect } from 'react-redux';
-import Modal from 'react-bootstrap/lib/Modal';
-import LocaleFormattedMessage from 'components/LocaleFormattedMessage';
-import ReciterDropdown from 'components/ReciterDropdown';
-import ContentDropdown from 'components/ContentDropdown';
-import TooltipDropdown from 'components/TooltipDropdown';
-import { setOption } from 'redux/actions/options.js';
-import { load } from 'redux/actions/verses.js';
-
-const ModalHeader = Modal.Header;
-const ModalTitle = Modal.Title;
-const ModalBody = Modal.Body;
-
-const SettingsModal = ({
- chapter,
- ayahIds,
- open,
- handleHide,
- options,
- setOption, // eslint-disable-line no-shadow
- load // eslint-disable-line no-shadow
-}) => {
- const handleOptionChange = (payload) => {
- setOption(payload);
-
- if (chapter) {
- const first = [...ayahIds][0];
- const last = [...ayahIds][[...ayahIds].length - 1];
- load(chapter.chapterNumber, first, last, { ...options, ...payload });
- }
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-SettingsModal.propTypes = {
- chapter: customProptypes.surahType,
- ayahIds: PropTypes.instanceOf(Set),
- open: PropTypes.bool,
- handleHide: PropTypes.func.isRequired,
- options: customProptypes.optionsType,
- setOption: PropTypes.func.isRequired,
- load: PropTypes.func.isRequired,
-};
-
-SettingsModal.defaultProps = {
- open: false
-};
-
-export default connect(state => ({
- options: state.options
-}), { setOption, load })(SettingsModal);
diff --git a/src/components/Share.tsx b/src/components/Share.tsx
new file mode 100644
index 000000000..8b7586c6c
--- /dev/null
+++ b/src/components/Share.tsx
@@ -0,0 +1,97 @@
+import React from 'react';
+import {
+ FacebookShareButton,
+ FacebookIcon,
+ TwitterShareButton,
+ TwitterIcon,
+} from 'react-share';
+import styled, { css } from 'styled-components';
+import PropTypes from 'prop-types';
+import { ChapterShape, VerseShape } from '../shapes';
+
+const inlineStyle = css`
+ display: inline-flex;
+`;
+
+const Container = styled.div<{ inline?: boolean }>`
+ position: relative;
+ top: 7px;
+ display: block;
+
+ ${prop => prop.inline && inlineStyle} .social-icon {
+ &:hover {
+ cursor: pointer;
+ opacity: 0.8;
+ }
+ }
+`;
+
+const FacebookButton = styled(FacebookShareButton)`
+ background-repeat: no-repeat;
+ background-size: 12px;
+ padding-top: 1px;
+ display: inline-block;
+ padding-right: 8px;
+`;
+
+const TwitterButton = styled(TwitterShareButton)`
+ background-repeat: no-repeat;
+ background-size: 21px;
+ display: inline-block;
+`;
+
+type Props = {
+ chapter: ChapterShape;
+ verse?: VerseShape;
+ inline?: boolean;
+};
+
+const Share: React.SFC = ({ chapter, verse, inline }: Props) => {
+ // Fallback to Surah Id
+ let path;
+
+ if (verse) {
+ const translations = (verse.translations || [])
+ .map(translation => translation.resourceId)
+ .join(',');
+ path = `${verse.chapterId}/${
+ verse.verseNumber
+ }?translations=${translations}`;
+ } else {
+ path = chapter.chapterNumber;
+ }
+
+ const shareUrl = `https://quran.com/${path}`;
+ const title = verse
+ ? `Surah ${chapter.nameSimple} [${verse.verseKey}]`
+ : `Surah ${chapter.nameSimple}`;
+ const iconProps = verse ? { iconBgStyle: { fill: '#d1d0d0' } } : {};
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+Share.propTypes = {
+ chapter: ChapterShape.isRequired,
+ verse: VerseShape.isRequired,
+ inline: PropTypes.bool,
+};
+
+Share.defaultProps = {
+ inline: false,
+};
+
+export default Share;
diff --git a/src/components/Share/index.js b/src/components/Share/index.js
deleted file mode 100644
index aefd5f1a7..000000000
--- a/src/components/Share/index.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import React from 'react';
-import { ShareButtons, generateShareIcon } from 'react-share';
-import * as customPropTypes from 'customPropTypes';
-
-const styles = require('./style.scss');
-
-const { FacebookShareButton, TwitterShareButton } = ShareButtons;
-const FacebookIcon = generateShareIcon('facebook');
-const TwitterIcon = generateShareIcon('twitter');
-
-const Share = ({ chapter, verse }) => {
- // Fallback to Surah Id
- let path;
-
- if (verse) {
- const translations = (verse.translations || [])
- .map(translation => translation.resourceId)
- .join(',');
- path = `${verse.chapterId}/${verse.verseNumber}?translations=${translations}`;
- } else {
- path = chapter.chapterNumber;
- }
-
- const shareUrl = `https://quran.com/${path}`;
- const title = verse
- ? `Surah ${chapter.nameSimple} [${verse.verseKey}]`
- : `Surah ${chapter.nameSimple}`;
- const iconProps = verse ? { iconBgStyle: { fill: '#d1d0d0' } } : {};
-
- return (
-
-
-
-
-
-
-
-
- );
-};
-
-Share.propTypes = {
- chapter: customPropTypes.surahType.isRequired,
- verse: customPropTypes.verseType
-};
-
-export default Share;
diff --git a/src/components/Share/style.scss b/src/components/Share/style.scss
deleted file mode 100644
index 92b6684bb..000000000
--- a/src/components/Share/style.scss
+++ /dev/null
@@ -1,42 +0,0 @@
-@import '../../styles/variables';
-
-.shareContainer {
- position: relative;
- top: 7px;
- display: inline-block;
-
- .iconContainer {
- display: inline-block;
-
- &:last-child{
- padding-left: 5px;
- }
-
- &:hover {
- cursor: pointer;
- }
- }
-
- .facebook {
- background-image: url(../../../static/images/FB-grn.png);
- background-repeat: no-repeat;
-
- background-size: 12px;
- padding-top: 1px;
-
- &:hover {
- background-image: url(../../../static/images/FB-beige.png);
- }
- }
-
- .twitter {
- background-image: url(../../../static/images/Twitter-grn.png);
- background-repeat: no-repeat;
- background-size: 21px;
-
- &:hover {
- background-image: url(../../../static/images/Twitter-beige.png);
- }
- }
-
-}
diff --git a/src/components/SmartBanner.tsx b/src/components/SmartBanner.tsx
new file mode 100644
index 000000000..a0086a684
--- /dev/null
+++ b/src/components/SmartBanner.tsx
@@ -0,0 +1,278 @@
+/* global window */
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import useragent from 'express-useragent';
+import toNumber from 'lodash/toNumber';
+import cookie from 'react-cookie';
+import StoreTextShape from '../shapes/StoreTextShape';
+
+const appleIcon = require('../../static/images/app-banner-ios.jpg');
+const androidIcon = require('../../static/images/app-banner-android.png');
+
+const ICONS: { [key: string]: string } = {
+ apple: appleIcon,
+ android: androidIcon,
+};
+
+const propTypes = {
+ daysHidden: PropTypes.number,
+ daysReminder: PropTypes.number,
+ appStoreLanguage: PropTypes.string,
+ button: PropTypes.string,
+ storeText: StoreTextShape,
+ price: StoreTextShape,
+ force: PropTypes.string,
+ title: PropTypes.string,
+ author: PropTypes.string,
+};
+
+const defaultProps = {
+ daysHidden: 15,
+ daysReminder: 90,
+ appStoreLanguage: 'us',
+ button: 'View',
+ storeText: {
+ ios: 'On the App Store',
+ android: 'In Google Play',
+ windows: 'In Windows Store',
+ kindle: 'In the Amazon Appstore',
+ },
+ price: {
+ ios: 'Free',
+ android: 'Free',
+ windows: 'Free',
+ kindle: 'Free',
+ },
+ force: '',
+ title: '',
+ author: '',
+};
+
+type Props = {
+ daysHidden?: number;
+ daysReminder?: number;
+ appStoreLanguage?: string;
+ button?: string;
+ storeText?: StoreTextShape;
+ price?: StoreTextShape;
+ force?: string;
+ title?: string;
+ author?: string;
+};
+
+class SmartBanner extends Component {
+ static propTypes = propTypes;
+ static defaultProps = defaultProps;
+
+ state = {
+ settings: {},
+ deviceType: '',
+ appId: '',
+ };
+
+ componentDidMount() {
+ const { force } = this.props;
+
+ if (__CLIENT__ && force) {
+ this.setSettings(force);
+ }
+ }
+
+ setSettings(forceDeviceType: string) {
+ const agent = useragent.parse(window.navigator.userAgent);
+ const osVersion = toNumber(agent.version);
+ let deviceType = '';
+
+ if (forceDeviceType) {
+ deviceType = forceDeviceType;
+ } else if (
+ (agent.isAndroid || agent.isAndroidTablet) &&
+ (agent.isChrome ? osVersion < 44 : true)
+ ) {
+ deviceType = 'android';
+ } else if (
+ (agent.isiPad || agent.isiPhone) &&
+ (agent.isSafari ? osVersion < 6 : true)
+ ) {
+ deviceType = 'ios';
+ }
+
+ this.setState({ deviceType });
+ if (deviceType) {
+ this.setSettingsForDevice(deviceType);
+ }
+ }
+
+ setSettingsForDevice(deviceType: string) {
+ const mixins: { [key: string]: { [key: string]: string | $TsFixMe } } = {
+ ios: {
+ icon: 'app-banner-ios.jpg',
+ appMeta: 'apple-itunes-app',
+ getStoreLink: () => {
+ const { appStoreLanguage } = this.props;
+
+ return `https://itunes.apple.com/${appStoreLanguage}/app/id`;
+ },
+ },
+ android: {
+ icon: 'app-banner-android.png',
+ appMeta: 'google-play-app',
+ getStoreLink: () => 'http://play.google.com/store/apps/details?id=',
+ },
+ };
+
+ if (mixins[deviceType]) {
+ this.setState({
+ settings: mixins[deviceType],
+ appId: this.parseAppId(mixins[deviceType].appMeta),
+ });
+ }
+ }
+
+ parseAppId = (metaName: string) => {
+ const meta = window.document.querySelector(`meta[name="${metaName}"]`);
+
+ if (meta && meta.getAttribute('content')) {
+ const content: $TsFixMe = meta.getAttribute('content');
+
+ return (/app-id=([^\s,]+)/ as $TsFixMe).exec(content)[1];
+ }
+
+ return null;
+ };
+
+ hide = () => {
+ if (window.document && window.document.querySelector('html')) {
+ (window.document.querySelector('html') as Element).classList.remove(
+ 'smartbanner-show'
+ );
+ }
+ };
+
+ show = () => {
+ if (window.document && window.document.querySelector('html')) {
+ (window.document.querySelector('html') as Element).classList.add(
+ 'smartbanner-show'
+ );
+ }
+ };
+
+ close() {
+ const { daysHidden } = this.props;
+
+ if (!daysHidden) return null;
+
+ this.hide();
+
+ let expireDate = new Date();
+ expireDate = new Date(
+ expireDate.setDate(expireDate.getDate() + daysHidden)
+ );
+
+ cookie.save('smartbanner-closed', 'true', {
+ path: '/',
+ expires: expireDate,
+ });
+
+ return null;
+ }
+
+ install() {
+ const { daysReminder } = this.props;
+
+ if (!daysReminder) return null;
+
+ let expireDate = new Date();
+ expireDate = new Date(
+ expireDate.setDate(expireDate.getDate() + daysReminder)
+ );
+
+ this.hide();
+ cookie.save('smartbanner-installed', 'true', {
+ path: '/',
+ expires: expireDate,
+ });
+
+ return null;
+ }
+
+ retrieveInfo() {
+ const { price, storeText } = this.props;
+ const { deviceType, settings, appId } = this.state;
+
+ const link = (settings as $TsFixMe).getStoreLink() + appId;
+ const inStore = `
+ ${(price as StoreTextShape & { [key: string]: string })[deviceType]} - ${
+ (storeText as StoreTextShape & { [key: string]: string })[deviceType]
+ }`;
+ const icon = ICONS[deviceType];
+
+ return {
+ icon,
+ link,
+ inStore,
+ };
+ }
+
+ render() {
+ const { author, title, button } = this.props;
+ const { deviceType, appId } = this.state;
+ // Don't show banner when:
+ // 1) if device isn't iOS or Android
+ // 2) website is loaded in app,
+ // 3) user dismissed banner,
+ // 4) or we have no app id in meta
+
+ if (
+ !deviceType ||
+ cookie.load('smartbanner-closed') ||
+ cookie.load('smartbanner-installed')
+ ) {
+ return null;
+ }
+
+ if (!appId) {
+ return null;
+ }
+
+ this.show();
+
+ const { icon, link, inStore } = this.retrieveInfo();
+ const wrapperClassName = `smartbanner smartbanner-${deviceType}`;
+ const iconStyle = {
+ backgroundImage: `url(${icon})`,
+ };
+
+ return (
+
+ );
+ }
+}
+
+export default SmartBanner;
diff --git a/src/components/SmartBanner/index.js b/src/components/SmartBanner/index.js
deleted file mode 100644
index 3dd954306..000000000
--- a/src/components/SmartBanner/index.js
+++ /dev/null
@@ -1,204 +0,0 @@
-/* global window */
-import React, { Component, PropTypes } from 'react';
-import * as customPropTypes from 'customPropTypes';
-import useragent from 'express-useragent';
-import cookie from 'react-cookie';
-
-class SmartBanner extends Component {
-
- state = {
- settings: {},
- deviceType: '',
- appId: ''
- };
-
- componentDidMount() {
- if (__CLIENT__) {
- this.setSettings(this.props.force);
- }
- }
-
- setSettings(forceDeviceType) {
- const agent = useragent.parse(window.navigator.userAgent);
- let deviceType = '';
- const osVersion = parseInt(agent.version, 10);
-
- if (forceDeviceType) {
- deviceType = forceDeviceType;
- } else if (
- (agent.isAndroid || agent.isAndroidTablet) &&
- (agent.isChrome ? osVersion < 44 : true)
- ) {
- deviceType = 'android';
- } else if ((agent.isiPad || agent.isiPhone) && (agent.isSafari ? osVersion < 6 : true)) {
- deviceType = 'ios';
- }
-
- this.setState({ deviceType });
- if (deviceType) {
- this.setSettingsForDevice(deviceType);
- }
- }
-
- setSettingsForDevice(deviceType) {
- const mixins = {
- ios: {
- icon: 'app-banner-ios.jpg',
- appMeta: 'apple-itunes-app',
- getStoreLink: () =>
- `https://itunes.apple.com/${this.props.appStoreLanguage}/app/id`,
- },
- android: {
- icon: 'app-banner-android.png',
- appMeta: 'google-play-app',
- getStoreLink: () =>
- 'http://play.google.com/store/apps/details?id=',
- }
- };
-
- if (mixins[deviceType]) {
- this.setState({
- settings: mixins[deviceType],
- appId: this.parseAppId(mixins[deviceType].appMeta)
- });
- }
- }
-
- parseAppId = (metaName) => {
- const meta = window.document.querySelector(`meta[name="${metaName}"]`);
- return /app-id=([^\s,]+)/.exec(meta.getAttribute('content'))[1];
- }
-
- hide = () => {
- window.document.querySelector('html').classList.remove('smartbanner-show');
- }
-
- show = () => {
- window.document.querySelector('html').classList.add('smartbanner-show');
- }
-
- close() {
- this.hide();
-
- let expireDate = new Date();
- expireDate = new Date(expireDate.setDate(expireDate.getDate() + this.props.daysHidden));
-
- cookie.save('smartbanner-closed', 'true', {
- path: '/',
- expires: expireDate,
- });
- }
-
- install() {
- let expireDate = new Date();
- expireDate = new Date(expireDate.setDate(expireDate.getDate() + this.props.daysReminder));
-
- this.hide();
- cookie.save('smartbanner-installed', 'true', {
- path: '/',
- expires: expireDate,
- });
- }
-
- retrieveInfo() {
- const link = this.state.settings.getStoreLink() + this.state.appId;
- const inStore = `
- ${this.props.price[this.state.deviceType]} - ${this.props.storeText[this.state.deviceType]}`;
- const icon = require(`../../../static/images/${this.state.settings.icon}`); // eslint-disable-line
-
- return {
- icon,
- link,
- inStore,
- };
- }
-
-
- render() {
- // Don't show banner when:
- // 1) if device isn't iOS or Android
- // 2) website is loaded in app,
- // 3) user dismissed banner,
- // 4) or we have no app id in meta
-
- if (!this.state.deviceType
- || window.navigator.standalone
- || cookie.load('smartbanner-closed')
- || cookie.load('smartbanner-installed')) {
- return null;
- }
-
- if (!this.state.appId) {
- return null;
- }
-
- this.show();
-
- const { icon, link, inStore } = this.retrieveInfo();
- const wrapperClassName = `smartbanner smartbanner-${this.state.deviceType}`;
- const iconStyle = {
- backgroundImage: `url(${icon})`,
- };
-
- return (
-
- );
- }
-}
-
-SmartBanner.propTypes = {
- daysHidden: PropTypes.number,
- daysReminder: PropTypes.number,
- appStoreLanguage: PropTypes.string,
- button: PropTypes.string,
- storeText: customPropTypes.storeText,
- price: customPropTypes.storeText,
- force: PropTypes.string,
- title: PropTypes.string,
- author: PropTypes.string,
-};
-
-SmartBanner.defaultProps = {
- daysHidden: 15,
- daysReminder: 90,
- appStoreLanguage: 'us',
- button: 'View',
- storeText: {
- ios: 'On the App Store',
- android: 'In Google Play',
- windows: 'In Windows Store',
- kindle: 'In the Amazon Appstore',
- },
- price: {
- ios: 'Free',
- android: 'Free',
- windows: 'Free',
- kindle: 'Free',
- },
- force: '',
- title: '',
- author: '',
-};
-
-export default SmartBanner;
diff --git a/src/components/SurahInfo/index.js b/src/components/SurahInfo/index.js
deleted file mode 100644
index 16d13b1b4..000000000
--- a/src/components/SurahInfo/index.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import React, { PropTypes } from 'react';
-import * as customPropTypes from 'customPropTypes';
-import Loader from 'quran-components/lib/Loader';
-
-const style = require('./style.scss');
-
-const SurahInfo = ({ chapter, info, isShowingSurahInfo, onClose }) => {
- // So we don't need to load images and files unless needed
- if (!isShowingSurahInfo) return ;
- if (!info) {
- return ;
- }
-
- return (
-
- {
- onClose &&
-
onClose({ isShowingSurahInfo: !isShowingSurahInfo })}
- />
- }
-
-
-
-
- VERSES
- {chapter.versesCount}
- PAGES
- {chapter.pages.join('-')}
-
-
-
-
-
-
-
- Source: {info.source}
-
-
-
-
-
-
- );
-};
-
-SurahInfo.propTypes = {
- onClose: PropTypes.func,
- isShowingSurahInfo: PropTypes.bool,
- chapter: customPropTypes.surahType,
- info: customPropTypes.infoType
-};
-
-export default SurahInfo;
diff --git a/src/components/SurahInfo/style.scss b/src/components/SurahInfo/style.scss
deleted file mode 100644
index 7fbf9b609..000000000
--- a/src/components/SurahInfo/style.scss
+++ /dev/null
@@ -1,115 +0,0 @@
-@import '../../styles/variables.scss';
-$transition-speed: 0.75s;
-
-.container{
- overflow-y: auto;
- margin-bottom: 30px;
- height: 0px;
- max-height: 0px;
- min-height: 0px;
- transition: max-height $transition-speed, height $transition-speed;
- margin-top: -20px; // To account for the .surah-container padding.
-
- @media(max-width: $screen-xs-max) {
- margin-bottom: 0;
- }
-
- &.show{
- max-height: 600px;
- max-height: 70vh;
- height: 1000px;
- }
-
- .row{
- height: 100%;
- }
-
- .bg{
- height: 100%;
- background-size: cover !important;
- background-position: center center;
- background-repeat: no-repeat;
-
- &.madinah{
- background-image: url('../../../static/images/madinah.jpg');
- }
-
- &.makkah{
- background-image: url('../../../static/images/makkah.jpg');
- }
- }
-
- .list{
- background: lighten($text-muted, 10%);
- height: 100%;
- }
-
- @media(max-width: $screen-sm){
- .list, .bg{
- height: 30%;
- }
- }
-
-
- .info{
- height: 100%;
- overflow-y: auto;
- -webkit-overflow-scrolling: touch;
- padding-top: 15px;
- padding-bottom: 15px;
- font-size: 16px;
- background: lighten($text-muted, 15%);
-
- h2{
- font-size: 22px;
- font-family: $font-montserrat;
- font-weight: bold;
- }
-
- h3{
- font-size: 20px;
- font-family: $font-montserrat;
- font-weight: bold;
- }
-
- p{
- font-size: 16px;
- }
- }
-
- dl{
- padding-top: 8px;
- text-align: right;
-
- dt{
- font-size: 10px;
- font-weight: 500;
- padding-top: 25px;
- padding-bottom: 5px;
- }
-
- dd{
- color: $brand-primary;
- font-weight: 300;
- }
- }
-
- .close{
- position: absolute;
- right: 15px;
- top: 15px;
- background: lighten($text-muted, 10%);
- height: 26px;
- width: 26px;
- padding: 7px 8px;
- font-size: 10px;
- border-radius: 16px;
- color: #fff;
- z-index: 20;
- cursor: pointer;
-
- &:hover{
- opacity: 0.8;
- }
- }
-}
diff --git a/src/components/SurahsDropdown/index.js b/src/components/SurahsDropdown/index.js
deleted file mode 100644
index 9f3b37d94..000000000
--- a/src/components/SurahsDropdown/index.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import React, { Component } from 'react';
-import * as customPropTypes from 'customPropTypes';
-import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
-import NavDropdown from 'react-bootstrap/lib/NavDropdown';
-import MenuItem from 'react-bootstrap/lib/MenuItem';
-import LocaleFormattedMessage from 'components/LocaleFormattedMessage';
-
-const styles = require('./style.scss');
-
-class SurahsDropdown extends Component {
- shouldComponentUpdate(nextProps) {
- return this.props.chapters !== nextProps.chapters;
- }
-
- renderList() {
- const { chapters } = this.props;
-
- return Object.values(chapters).map((chapter, index) => (
-
-
-
-
-
- {chapter.chapterNumber}
-
-
-
- {chapter.nameSimple}
-
- {chapter.translatedName.name}
-
-
- {chapter.nameArabic}
-
-
-
-
- ));
- }
-
- render() {
- const { chapter } = this.props;
-
- return (
- }
- >
- {this.renderList()}
-
- );
- }
-}
-
-SurahsDropdown.propTypes = {
- chapters: customPropTypes.chapters.isRequired,
- chapter: customPropTypes.chapters.isRequired,
-};
-
-export default SurahsDropdown;
diff --git a/src/components/SurahsDropdown/style.scss b/src/components/SurahsDropdown/style.scss
deleted file mode 100644
index a8c0d7443..000000000
--- a/src/components/SurahsDropdown/style.scss
+++ /dev/null
@@ -1,14 +0,0 @@
-.dropdown{
- :global(.dropdown-menu){
- max-height: 400px;
- max-height: 60vh;
- overflow-y: scroll;
- overflow-x: hidden;
- -webkit-overflow-scrolling: touch;
- }
-}
-
-.arabicName {
- direction: rtl;
- padding-right: 5px;
-}
diff --git a/src/components/SwitchToggle/index.js b/src/components/SwitchToggle/index.js
deleted file mode 100644
index 26451b0e0..000000000
--- a/src/components/SwitchToggle/index.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import React, { PropTypes } from 'react';
-
-const styles = require('./style.scss');
-
-const SwitchToggle = ({ id, flat, checked, onToggle }) => (
-
-
-
-
-);
-
-SwitchToggle.propTypes = {
- id: PropTypes.string,
- flat: PropTypes.bool,
- checked: PropTypes.bool,
- onToggle: PropTypes.func.isRequired
-};
-
-SwitchToggle.defaultProps = {
- flat: false,
- checked: false
-};
-
-export default SwitchToggle;
diff --git a/src/components/SwitchToggle/style.scss b/src/components/SwitchToggle/style.scss
deleted file mode 100644
index 0d21d4bbb..000000000
--- a/src/components/SwitchToggle/style.scss
+++ /dev/null
@@ -1,106 +0,0 @@
-@import '../../styles/variables.scss';
-$old-color: #8ce196;
-$color: $brand-primary;
-$height: 25px;
-$width: 50px;
-
-.switch{
- display: inline-block;
- vertical-align: middle;
-}
-
-.toggle {
- position: absolute;
- margin-left: -9999px;
- visibility: hidden;
-}
-
-.toggle + .label {
- display: block;
- position: relative;
- cursor: pointer;
- outline: none;
- user-select: none;
- margin-bottom: 0px;
-}
-
-.toggleRound + .label {
- padding: 2px;
- width: $width;
- height: $height;
- background-color: #dddddd;
- border-radius: $height;
-}
-
-.toggleRound + .label:before,
-.toggleRound + .label:after {
- display: block;
- position: absolute;
- top: 1px;
- left: 1px;
- bottom: 1px;
- content: "";
-}
-
-.toggleRound + .label:before {
- right: 1px;
- background-color: #f1f1f1;
- border-radius: $height;
- transition: background 0.4s;
-}
-
-.toggleRound + .label:after {
- width: $height - 2;
- background-color: #fff;
- border-radius: 100%;
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
- transition: margin 0.4s;
-}
-
-.toggleRound:checked + .label:before {
- background-color: $color;
-}
-
-.toggleRound:checked + .label:after {
- margin-left: $width / 2;
-}
-
-.toggleFlat + .label {
- padding: 2px;
- width: $width;
- height: $height;
- background-color: #dddddd;
- border-radius: $height;
- transition: background 0.4s;
-}
-.toggleFlat + .label:before,
-.toggleFlat + .label:after {
- display: block;
- position: absolute;
- content: "";
-}
-.toggleFlat + .label:before {
- top: 2px;
- left: 2px;
- bottom: 2px;
- right: 2px;
- background-color: #fff;
- border-radius: $height;
- transition: background 0.4s;
-}
-.toggleFlat + .label:after {
- top: 4px;
- left: 4px;
- bottom: 4px;
- width: $height - 8;
- background-color: #dddddd;
- border-radius: $height - 8;
- transition: margin 0.4s, background 0.4s;
-}
-.toggleFlat:checked + .label {
- background-color: $color;
-}
-.toggleFlat:checked + .label:after {
- margin-left: $width / 2;
- background-color: $color;
-}
diff --git a/src/components/T.tsx b/src/components/T.tsx
new file mode 100644
index 000000000..a30f6726a
--- /dev/null
+++ b/src/components/T.tsx
@@ -0,0 +1,43 @@
+import React, { ReactNode } from 'react';
+import PropTypes from 'prop-types';
+import {
+ injectIntl,
+ intlShape,
+ FormattedMessage,
+ InjectedIntlProps, // eslint-disable-line
+} from 'react-intl';
+import config from '../../config';
+import LOCALE_KEYS from '../locale/keys';
+
+type Props = {
+ id: LOCALE_KEYS;
+ values?: { [key: string]: string };
+ intl?: $TsFixMe;
+ children?: (...formattedMessage: Array) => ReactNode;
+};
+
+const { en } = config('localeMessages');
+
+const T: React.SFC = ({
+ id,
+ values,
+ children,
+ intl,
+}: Props & InjectedIntlProps) => (
+
+
+ {children}
+
+
+);
+
+T.propTypes = {
+ id: PropTypes.string.isRequired,
+ intl: intlShape.isRequired,
+ values: PropTypes.object, // eslint-disable-line
+ children: PropTypes.node, // eslint-disable-line
+};
+
+export const KEYS = LOCALE_KEYS;
+
+export default injectIntl(T);
diff --git a/src/components/Tafsir.tsx b/src/components/Tafsir.tsx
new file mode 100644
index 000000000..d5bdfb8b2
--- /dev/null
+++ b/src/components/Tafsir.tsx
@@ -0,0 +1,40 @@
+import React from 'react';
+
+import Loader from 'quran-components/lib/Loader';
+import { TafsirShape, VerseShape } from '../shapes';
+
+const propTypes = {
+ tafsir: TafsirShape,
+ verse: VerseShape.isRequired,
+};
+
+const defaultProps = {
+ tafsir: null,
+};
+
+type Props = {
+ tafsir?: TafsirShape | null;
+ verse: VerseShape;
+};
+
+const Tafsir: React.SFC = ({ tafsir, verse }: Props) => {
+ if (!tafsir) {
+ return ;
+ }
+
+ return (
+
+
{tafsir.resourceName}
+
{verse.textMadani}
+
+
+ );
+};
+
+Tafsir.propTypes = propTypes;
+Tafsir.defaultProps = defaultProps;
+
+export default Tafsir;
diff --git a/src/components/TooltipDropdown/index.js b/src/components/TooltipDropdown/index.js
deleted file mode 100644
index 0f9d407be..000000000
--- a/src/components/TooltipDropdown/index.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import React, { PropTypes } from 'react';
-import Menu, { MenuItem } from 'quran-components/lib/Menu';
-import Radio from 'quran-components/lib/Radio';
-import Icon from 'quran-components/lib/Icon';
-import LocaleFormattedMessage from 'components/LocaleFormattedMessage';
-
-const TooltipDropdown = ({ tooltip, onOptionChange }) => {
- const handleOptionChange = type => onOptionChange({
- tooltip: type
- });
-
- const list = ['translation', 'transliteration'].map(type => (
-
- handleOptionChange(type)}
- >
-
-
-
- ));
-
- return (
- }
- menu={
-
- {list}
-
- }
- >
-
-
- );
-};
-
-TooltipDropdown.propTypes = {
- onOptionChange: PropTypes.func,
- tooltip: PropTypes.string.isRequired,
-};
-
-export default TooltipDropdown;
diff --git a/src/components/TooltipDropdown/style.scss b/src/components/TooltipDropdown/style.scss
deleted file mode 100644
index 298b6cf11..000000000
--- a/src/components/TooltipDropdown/style.scss
+++ /dev/null
@@ -1,18 +0,0 @@
-@import '../../styles/variables.scss';
-
-:local .popover{
- :global(.popover-title){
- font-family: $font-montserrat;
- text-transform: uppercase;
- color: $cream;
- padding-top: 15px;
- padding-bottom: 15px;
- font-size: 0.75em;
- }
-
- :global(.popover-content){
- :global(a){
- font-size: 0.8em;
- }
- }
-}
diff --git a/src/components/TopOptions/index.js b/src/components/TopOptions/index.js
deleted file mode 100644
index 835a348ae..000000000
--- a/src/components/TopOptions/index.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import React, { PropTypes } from 'react';
-import Share from 'components/Share';
-import * as customPropTypes from 'customPropTypes';
-
-import styles from './style.scss';
-
-const TopOptions = ({ title, chapter }) => (
-
-
- {/* NOTE: Caused about 7000 lines of code to accept all titles SVG */}
- {/*
*/}
- {title && {title} }
-
-
-
-);
-
-TopOptions.propTypes = {
- title: PropTypes.string,
- chapter: customPropTypes.surahType.isRequired
-};
-
-export default TopOptions;
diff --git a/src/components/TopOptions/spec.js b/src/components/TopOptions/spec.js
deleted file mode 100644
index 4a6c55174..000000000
--- a/src/components/TopOptions/spec.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-
-import TopOptions from './index.js';
-import getSurahs from '../../../tests/fixtures/getSurahs.js';
-// used components
-import InformationToggle from 'components/InformationToggle'; // eslint-disable-line
-import FontSizeDropdown from 'components/FontSizeDropdown'; // eslint-disable-line
-import TooltipDropdown from 'components/TooltipDropdown'; // eslint-disable-line
-import ReadingModeToggle from 'components/ReadingModeToggle'; // eslint-disable-line
-import Share from 'components/Share'; // eslint-disable-line
-
-describe(' ', () => {
- it('Should render QuickSurahs component', () => {
- const options = {
- isReadingMode: false,
- isShowingSurahInfo: false,
- tooltip: 'translation',
- fontSize: {}
- };
-
- const actions = {
- options: {
- setOption: () => {},
- toggleReadingMode: () => {}
- }
- };
-
- const component = shallow(
-
- );
-
- expect(component).to.be.ok; // eslint-disable-line
- expect(component.find(Share).length).to.eql(1);
- });
-});
diff --git a/src/components/TopOptions/style.scss b/src/components/TopOptions/style.scss
deleted file mode 100644
index 2ea5edba8..000000000
--- a/src/components/TopOptions/style.scss
+++ /dev/null
@@ -1,13 +0,0 @@
-.title {
- width: 45%;
- position: absolute;
- top: 25px;
- transform: translateY(-47%);
- display: inline-block;
- vertical-align: middle;
-}
-
-.titleText {
- color: #000;
- font-size: 18px;
-}
diff --git a/src/components/Translation.tsx b/src/components/Translation.tsx
new file mode 100644
index 000000000..27149eacf
--- /dev/null
+++ b/src/components/Translation.tsx
@@ -0,0 +1,75 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import TranslationNode from './TranslationNode';
+import { TranslationShape } from '../shapes';
+import { FetchFootNote } from '../redux/actions/footNotes';
+
+const TranslationText = styled.small<{ isNightMode?: boolean }>`
+ font-family: ${({ theme }) => theme.fonts.timesNew};
+ color: ${({ theme, isNightMode }) =>
+ isNightMode ? theme.colors.white : theme.colors.gray};
+`;
+
+const propTypes = {
+ translation: TranslationShape.isRequired,
+ fetchFootNote: PropTypes.func.isRequired,
+ verseKey: PropTypes.string.isRequired,
+ isNightMode: PropTypes.bool,
+};
+
+type Props = {
+ translation: TranslationShape;
+ fetchFootNote: FetchFootNote;
+ verseKey: string;
+ isNightMode: boolean;
+};
+
+class Translation extends Component {
+ public static propTypes = propTypes;
+
+ handleNodeClick = (event: $TsFixMe) => {
+ const { fetchFootNote, verseKey } = this.props;
+
+ if (
+ event.target.nodeName === 'SUP' &&
+ event.target.attributes.foot_note &&
+ fetchFootNote
+ ) {
+ event.preventDefault();
+
+ fetchFootNote({
+ footNoteId: event.target.attributes.foot_note.value,
+ verseKey,
+ });
+ }
+ };
+
+ render() {
+ const { translation, isNightMode } = this.props;
+ const lang = translation.languageName;
+ const isArabic = lang === 'arabic';
+
+ return (
+
+ {translation.resourceName}
+
+
+
+
+ );
+ }
+}
+
+export default Translation;
diff --git a/src/components/Translation/index.js b/src/components/Translation/index.js
deleted file mode 100644
index 7d948be3f..000000000
--- a/src/components/Translation/index.js
+++ /dev/null
@@ -1,68 +0,0 @@
-/* eslint-disable react/prefer-stateless-function */
-import React, { Component, PropTypes } from 'react';
-import * as customPropTypes from 'customPropTypes';
-import { connect } from 'react-redux';
-import { loadFootNote } from 'redux/actions/footNote';
-
-const styles = require('./style.scss');
-
-class Translation extends Component {
-
- componentDidMount() {
- const { index } = this.props;
- let trans;
-
- if (__CLIENT__) {
- trans = document.getElementById(`trans${index}`).children[1]; // eslint-disable-line no-undef
- trans.addEventListener('click', this.fetchFootNote, true);
- }
- }
-
- componentWillUnmount() {
- // TODO: this is breaking for search! Need to figure out why
- // const { index } = this.props;
- // let trans;
-
- // if (__CLIENT__) {
- // trans = document.getElementById(`trans${index}`).children[1]; // eslint-disable-line
- // trans.removeEventListener('click', this.fetchFootNote, true);
- // }
- }
-
- fetchFootNote = (event) => {
- const { loadFootNote } = this.props; // eslint-disable-line no-shadow
-
- if (event.target.nodeName === 'SUP' && event.target.attributes.foot_note) {
- event.preventDefault();
- loadFootNote(event.target.attributes.foot_note.value);
- }
- }
-
- render() {
- const { translation, index } = this.props;
- const lang = translation.languageName;
- const isArabic = lang === 'arabic';
-
- return (
-
-
{translation.resourceName}
-
-
-
-
- );
- }
-}
-
-Translation.propTypes = {
- translation: customPropTypes.translationType.isRequired,
- index: PropTypes.number,
- loadFootNote: PropTypes.func.isRequired,
-};
-
-export default connect(state => ({}), // eslint-disable-line no-unused-vars
- { loadFootNote }
-)(Translation);
diff --git a/src/components/Translation/style.scss b/src/components/Translation/style.scss
deleted file mode 100644
index 28d7d5b68..000000000
--- a/src/components/Translation/style.scss
+++ /dev/null
@@ -1,29 +0,0 @@
-@import '../../styles/variables';
-
-.translation{
- &.arabic{
- text-align: right;
- }
-
- h4{
- color: $light-green;
- margin-bottom: 5px;
- text-transform: uppercase;
- font-size: 14px;
- font-weight: 400;
-
- @media(max-width: $screen-sm-max) {
- font-size: 12px;
- }
- }
-
- h2{
- margin-top: 5px;
- margin-bottom: 25px;
- }
-
- sup{
- color: $light-green;
- cursor: pointer;
- }
-}
diff --git a/src/components/TranslationNode.tsx b/src/components/TranslationNode.tsx
new file mode 100644
index 000000000..a85b4bc0b
--- /dev/null
+++ b/src/components/TranslationNode.tsx
@@ -0,0 +1,24 @@
+import styled from 'styled-components';
+
+export default styled.div<{ arabic?: boolean }>`
+ ${({ arabic }) => (arabic ? 'text-align: right;' : '')} h4 {
+ color: ${({ theme }) => theme.brandPrimary};
+ margin-bottom: 5px;
+ text-transform: uppercase;
+ font-size: 14px;
+ font-weight: 400;
+ display: block;
+ clear: both;
+ @media (max-width: ${({ theme }) => theme.breakpointsMd}) {
+ font-size: 12px;
+ }
+ }
+ h2 {
+ margin-top: 5px;
+ margin-bottom: 25px;
+ }
+ sup {
+ color: ${({ theme }) => theme.brandPrimary};
+ cursor: pointer;
+ }
+`;
diff --git a/src/components/Verse.tsx b/src/components/Verse.tsx
new file mode 100644
index 000000000..283521fe5
--- /dev/null
+++ b/src/components/Verse.tsx
@@ -0,0 +1,165 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import Element from 'react-scroll/modules/components/Element';
+import Translation from './Translation';
+
+import { VerseShape, ChapterShape, FootNoteShape } from '../shapes';
+import Text from './verse/Text';
+import Controls from './verse/Controls';
+import { FetchTafsirs } from '../redux/actions/tafsirs';
+import { SetCurrentVerseKey, Play, Pause } from '../redux/actions/audioplayer';
+import { FetchFootNote } from '../redux/actions/footNotes';
+import FootNote from './FootNote';
+
+const backgroundColor = ({
+ highlight,
+ isNightMode,
+}: {
+ highlight?: boolean;
+ isNightMode?: boolean;
+}): string => {
+ if (highlight) {
+ return isNightMode ? '#151414' : '#F5FBF7';
+ }
+
+ return '';
+};
+
+// TODO: Change this
+const VerseNode = styled(Element)<{
+ highlight?: boolean;
+ isNightMode?: boolean;
+ textMuted: string;
+}>`
+ padding: 2.5% 0;
+ border-bottom: 1px solid rgba(${({ textMuted }) => textMuted}, 0.5);
+ background-color: ${backgroundColor};
+ .text-info {
+ color: ${({ theme }) => theme.brandInfo};
+ &:hover {
+ color: ${({ theme }) => theme.brandPrimary};
+ }
+ }
+ a {
+ color: ${({ theme, isNightMode }) =>
+ isNightMode ? theme.colors.white : theme.colors.textColor};
+ }
+`;
+
+const propTypes = {
+ isSearched: PropTypes.bool,
+ verse: VerseShape.isRequired,
+ chapter: ChapterShape.isRequired,
+ isCurrentVersePlaying: PropTypes.bool.isRequired,
+ fetchTafsirs: PropTypes.func.isRequired,
+ pause: PropTypes.func.isRequired,
+ setCurrentVerseKey: PropTypes.func.isRequired,
+ fetchFootNote: PropTypes.func.isRequired,
+ isPdf: PropTypes.bool,
+ footNote: FootNoteShape,
+ isNightMode: PropTypes.bool,
+};
+
+const defaultProps: $TsFixMe = {
+ isSearched: false,
+ isPdf: false,
+ match: null,
+ currentVerse: null,
+ footNote: null,
+ isNightMode: false,
+};
+
+type Props = {
+ isSearched?: boolean;
+ verse: VerseShape;
+ chapter: ChapterShape;
+ isCurrentVersePlaying: boolean;
+ tooltip: 'translation' | 'transliteration';
+ fetchTafsirs: FetchTafsirs;
+ play: Play;
+ pause: Pause;
+ setCurrentVerseKey: SetCurrentVerseKey;
+ fetchFootNote: FetchFootNote;
+ isPdf?: boolean;
+ footNote?: FootNoteShape;
+ isNightMode: boolean;
+};
+
+class Verse extends Component {
+ public static propTypes = propTypes;
+
+ public static defaultProps = defaultProps;
+
+ handlePlay = () => {
+ const {
+ isCurrentVersePlaying,
+ verse,
+ pause,
+ setCurrentVerseKey,
+ } = this.props;
+
+ if (isCurrentVersePlaying) {
+ pause();
+ } else {
+ // automatically plays verse (again if paused)
+ setCurrentVerseKey(verse.verseKey, true);
+ }
+ };
+
+ handleTafsirsClick = () => {
+ const { verse, fetchTafsirs } = this.props;
+ // TODO: Fix this
+
+ return fetchTafsirs(verse.chapterId, verse.id, 1);
+ };
+
+ render() {
+ const {
+ verse,
+ footNote,
+ isCurrentVersePlaying,
+ isSearched,
+ chapter,
+ isPdf,
+ fetchFootNote,
+ isNightMode,
+ } = this.props;
+
+ const translations: Array<$TsFixMe> = verse.translations || [];
+
+ return (
+
+
+
+
+ {translations.map(translation => (
+
+ ))}
+ {footNote && }
+
+
+ );
+ }
+}
+
+export default Verse;
diff --git a/src/components/Verse/index.js b/src/components/Verse/index.js
deleted file mode 100644
index de3d77c60..000000000
--- a/src/components/Verse/index.js
+++ /dev/null
@@ -1,322 +0,0 @@
-import React, { Component, PropTypes } from 'react';
-import * as customPropTypes from 'customPropTypes';
-import Link from 'react-router/lib/Link';
-import Element from 'react-scroll/lib/components/Element';
-import Loadable from 'react-loadable';
-import ComponentLoader from 'components/ComponentLoader';
-import LocaleFormattedMessage from 'components/LocaleFormattedMessage';
-import Word from 'components/Word';
-import Translation from 'components/Translation';
-import debug from 'helpers/debug';
-
-const styles = require('./style.scss');
-
-const Copy = Loadable({
- loader: () => import('components/Copy'),
- LoadingComponent: ComponentLoader
-});
-
-const Share = Loadable({
- loader: () => import('components/Share'),
- LoadingComponent: ComponentLoader
-});
-
-class Verse extends Component {
- shouldComponentUpdate(nextProps) {
- const conditions = [
- this.props.verse !== nextProps.verse,
- this.props.bookmarked !== nextProps.bookmarked,
- this.props.tooltip !== nextProps.tooltip,
- this.props.currentWord !== nextProps.currentWord,
- this.props.iscurrentVerse !== nextProps.iscurrentVerse
- ];
-
- if (this.props.match) {
- conditions.push(this.props.match.length !== nextProps.match.length);
- }
-
- return conditions.some(condition => condition);
- }
-
- handlePlay(verse) {
- const { isPlaying, audioActions, iscurrentVerse } = this.props;
- const { pause, setAyah, play } = audioActions;
-
- if (isPlaying) {
- pause();
- }
-
- if (iscurrentVerse) {
- return;
- }
-
- setAyah(verse.verseKey);
- play();
- }
-
- renderTranslations() {
- const { verse, match } = this.props;
- const array = match || verse.translations || [];
-
- return array.map(translation => (
-
- ));
- }
-
- renderMedia() {
- const { verse, mediaActions, isSearched, isPdf } = this.props;
-
- if (isSearched || !verse.mediaContents) return false;
- if (isPdf) return false;
-
- return (
-
- {verse.mediaContents.map((content, index) => (
-
- ))}
-
- );
- }
-
- renderText() {
- const {
- verse,
- tooltip,
- currentVerse,
- isPlaying,
- audioActions,
- isSearched
- } = this.props; // eslint-disable-line max-len
- // NOTE: Some 'word's are glyphs (jeem). Not words and should not be clicked for audio
- let wordAudioPosition = -1;
- const renderText = false; // userAgent.isBot;
-
- const text = verse.words.map((word) => ( // eslint-disable-line
-
- ));
-
- return (
-
-
- {text}
-
-
- );
- }
-
- renderPlayLink() {
- const { isSearched, verse, currentVerse, isPlaying, isPdf } = this.props;
- const playing = verse.verseKey === currentVerse && isPlaying;
-
- if (isPdf) return false;
-
- if (!isSearched) {
- return (
- this.handlePlay(verse)}
- className="text-muted"
- >
-
- {' '}
-
-
- );
- }
-
- return false;
- }
-
- renderCopyLink() {
- const { isSearched, verse, isPdf } = this.props;
-
- if (isPdf) return false;
-
- if (!isSearched) {
- return ;
- }
-
- return false;
- }
-
- renderBookmark() {
- const {
- verse,
- bookmarked,
- isAuthenticated,
- bookmarkActions,
- isSearched
- } = this.props;
-
- if (isSearched || !isAuthenticated) return false;
-
- if (bookmarked) {
- return (
- bookmarkActions.removeBookmark(verse.verseKey)}
- className="text-muted"
- >
-
- {' '}
-
-
-
- );
- }
-
- return (
- bookmarkActions.addBookmark(verse.verseKey)}
- className="text-muted"
- >
- {' '}
-
-
- );
- }
-
- renderBadge() {
- const { isSearched, verse } = this.props;
- const translations = (verse.translations || [])
- .map(translation => translation.resourceId)
- .join(',');
- let metric;
-
- const content = (
-
-
- {verse.verseKey}
-
-
- );
-
- if (isSearched) {
- metric = 'Verse:Searched:Link';
- } else {
- metric = 'Verse:Link';
- }
-
- return (
-
- {content}
-
- );
- }
-
- renderShare() {
- const { isSearched, verse, chapter } = this.props;
-
- if (isSearched) return false;
-
- return ;
- }
-
- renderControls() {
- const { isPdf } = this.props;
-
- return (
-
- {this.renderBadge()}
- {this.renderPlayLink()}
- {this.renderCopyLink()}
- {this.renderBookmark()}
- {!isPdf && this.renderShare()}
-
- );
- }
-
- render() {
- const { verse, iscurrentVerse } = this.props;
- debug('component:Verse', `Render ${verse.verseKey}`);
-
- return (
-
- {this.renderControls()}
-
- {this.renderText()}
- {this.renderTranslations()}
- {this.renderMedia()}
-
-
- );
- }
-}
-
-Verse.propTypes = {
- isSearched: PropTypes.bool,
- verse: customPropTypes.verseType.isRequired,
- chapter: customPropTypes.surahType.isRequired,
- bookmarked: PropTypes.bool, // TODO: Add this for search
- bookmarkActions: customPropTypes.bookmarkActions,
- mediaActions: customPropTypes.mediaActions,
- audioActions: customPropTypes.audioActions,
- match: customPropTypes.match,
- isPlaying: PropTypes.bool,
- isAuthenticated: PropTypes.bool,
- tooltip: PropTypes.string,
- currentWord: PropTypes.number, // gets passed in an integer, null by default
- iscurrentVerse: PropTypes.bool,
- currentVerse: PropTypes.string,
- userAgent: PropTypes.object, // eslint-disable-line
- isPdf: PropTypes.bool
-};
-
-Verse.defaultProps = {
- currentWord: null,
- isSearched: false,
- isPdf: false
-};
-
-export default Verse;
diff --git a/src/components/Verse/spec.js b/src/components/Verse/spec.js
deleted file mode 100644
index 765bd7db8..000000000
--- a/src/components/Verse/spec.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-
-import Ayah from './index';
-import ayah from '../../../tests/fixtures/ayah';
-
-let wrapper;
-
-describe(' ', () => {
- beforeEach(() => {
- wrapper = shallow( {} }} />);
- });
-
- it('should render', () => {
- expect(wrapper).to.be.ok; // eslint-disable-line
- });
-
- it('should have correct ayah number', () => {
- expect(wrapper.find('.label').text()).to.eql(ayah.verseKey);
- });
-
- it('should contain translations', () => {
- expect(wrapper.find('.translation').text()).to.eql(ayah.content[0].resource.name);
- });
-});
diff --git a/src/components/Verse/style.scss b/src/components/Verse/style.scss
deleted file mode 100644
index 505d22c7b..000000000
--- a/src/components/Verse/style.scss
+++ /dev/null
@@ -1,145 +0,0 @@
-@import '../../styles/variables';
-@import '../../styles/partials/_tooltip';
-
-.container{
- padding: 2.5% 0%;
- border-bottom: 1px solid rgba($text-muted, 0.5);
-
- .text-info{
- color: $brand-info;
- &:hover{
- color: $brand-primary;
- }
- }
-
- &:hover{
- // background-color: rgba($brand-info, 0.25);
- .toggle-copy{
- visibility: visible;
- }
- }
-
- .toggle-copy{
- visibility: hidden;
- }
-
- .controls{
- a{
- margin-bottom: 15px;
- display: block;
- text-decoration: none;
- font-size: 12px;
- cursor: pointer;
-
- &:focus{
- color: $text-muted;
- }
- }
- .label{
- padding: .65em 1.1em;
- border-radius: 0px;
- display: inline-block;
- margin-bottom: 15px;
- font-weight: 300;
- color: $text-color;
-
- &:hover{
- opacity: 0.7;
- }
- }
-
- @media (max-width: $screen-xs-max) {
- h4,
- a{
- display: inline-block;
- margin: 0px 10px;
- }
-
- h4 {
- margin: 0;
- }
-
- padding: 0;
- }
- }
-}
-
-.font{
- white-space: pre-line;
- color: #000;
- width: 100%;
- overflow-wrap: break-word;
- line-height: 1.5;
- word-break: break-all;
- text-align: right;
- float: left;
-
- b, span{
- border-color: transparent;
- border-width: 0px 0px 1px 0px;
- border-style: solid;
- float: right;
- &.active {
- color: $brand-primary-darker-5;
- border-color: $brand-primary-darker-15;
- }
- }
-
- .line{
- direction: rtl;
- b, span{
- float: none;
- display: inline-block;
- }
- }
-
- b, a{
- font-weight: 100;
- padding: 0px 2px;
- color: #000;
- &:hover{
- color: $brand-primary;
- cursor: help;
- }
- &:focus{
- color: $brand-primary-darker-10;
- outline: none;
- }
- }
-
- p{
- display: block;
- clear: both;
- text-align: right;
- direction: rtl;
- float: right;
- }
-
- @media (max-width: $screen-xs-max) {
- font-size: 300%;
- line-height: 130%;
- }
-}
-
-.translation{
- h4{
- color: $light-green;
- margin-bottom: 5px;
- }
-
- h2{
- margin-top: 5px;
- margin-bottom: 25px;
- }
-}
-
-.word_font{
- line-height: 150%;
-}
-
-.line{
- line-height: 150%;
- display: block;
- width: 100%;
- margin: 0px auto;
-}
diff --git a/src/components/VerseCopy.tsx b/src/components/VerseCopy.tsx
new file mode 100644
index 000000000..de3d34c9c
--- /dev/null
+++ b/src/components/VerseCopy.tsx
@@ -0,0 +1,60 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import copyToClipboard from 'copy-to-clipboard';
+import T, { KEYS } from './T';
+
+import { COPY_EVENTS } from '../events';
+import ButtonLink from './dls/ButtonLink';
+
+const propTypes = {
+ text: PropTypes.string.isRequired,
+ verseKey: PropTypes.string.isRequired,
+};
+
+type Props = {
+ text: string;
+ verseKey: string;
+};
+
+type State = {
+ isCopied: boolean;
+};
+
+class VerseCopy extends Component {
+ public static propTypes = propTypes;
+
+ state = {
+ isCopied: false,
+ };
+
+ handleCopy = () => {
+ const { text, verseKey } = this.props;
+
+ copyToClipboard(`${text} - ${verseKey}`);
+ this.setState({ isCopied: true });
+
+ setTimeout(() => this.setState({ isCopied: false }), 1000);
+ };
+
+ render() {
+ const { verseKey } = this.props;
+ const { isCopied } = this.state;
+
+ return (
+
+ {' '}
+
+
+ );
+ }
+}
+
+export default VerseCopy;
diff --git a/src/components/VersesDropdown.tsx b/src/components/VersesDropdown.tsx
new file mode 100644
index 000000000..96ba009a0
--- /dev/null
+++ b/src/components/VersesDropdown.tsx
@@ -0,0 +1,84 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import range from 'lodash/range';
+import NavDropdown from 'react-bootstrap/lib/NavDropdown';
+import MenuItem from 'react-bootstrap/lib/MenuItem';
+import { Link } from 'react-scroll';
+import T, { KEYS } from './T';
+import { ChapterShape, VerseShape } from '../shapes';
+
+const StyledDropdown = styled(NavDropdown)`
+ .dropdown-menu {
+ max-height: 400px;
+ max-height: 60vh;
+ overflow-y: scroll;
+ -webkit-overflow-scrolling: touch;
+ overflow-x: hidden;
+ }
+`;
+
+const propTypes = {
+ chapter: ChapterShape.isRequired,
+ onClick: PropTypes.func.isRequired,
+ isReadingMode: PropTypes.bool.isRequired,
+ verses: PropTypes.shape({
+ verseKey: VerseShape,
+ }).isRequired,
+};
+
+type Props = {
+ chapter?: ChapterShape;
+ onClick(verseKey: string): void;
+ isReadingMode: boolean;
+ verses?: { [verseKey: string]: VerseShape };
+};
+
+const VersesDropdown: React.SFC = ({
+ chapter,
+ isReadingMode,
+ onClick,
+ verses,
+}: Props) => {
+ if (!chapter || !verses) return null;
+
+ const title = ;
+ const array = range(chapter.versesCount).map(
+ number => `${chapter.chapterNumber}:${number + 1}`
+ );
+
+ return (
+
+ {array.map(verseKey => {
+ if (verses[verseKey] && !isReadingMode) {
+ return (
+
+ onClick(verseKey)}
+ to={`verse:${verseKey}`}
+ smooth
+ spy
+ offset={-120}
+ activeClass="active"
+ duration={500}
+ className="pointer"
+ >
+ {verseKey}
+
+
+ );
+ }
+
+ return (
+ onClick(verseKey)}>
+ {verseKey}
+
+ );
+ })}
+
+ );
+};
+
+VersesDropdown.propTypes = propTypes;
+
+export default VersesDropdown;
diff --git a/src/components/VersesDropdown/index.js b/src/components/VersesDropdown/index.js
deleted file mode 100644
index 2c62b61f7..000000000
--- a/src/components/VersesDropdown/index.js
+++ /dev/null
@@ -1,80 +0,0 @@
-import React, { Component, PropTypes } from 'react';
-import * as customPropTypes from 'customPropTypes';
-import NavDropdown from 'react-bootstrap/lib/NavDropdown';
-import MenuItem from 'react-bootstrap/lib/MenuItem';
-import { Link } from 'react-scroll';
-import LocaleFormattedMessage from 'components/LocaleFormattedMessage';
-
-const style = require('./style.scss');
-
-class VersesDropdown extends Component {
- renderItem = (ayah, index) => {
- const { chapter, loadedVerses, isReadingMode, onClick } = this.props;
- const number = index + 1;
-
- if (loadedVerses.has(number) && !isReadingMode) {
- return (
-
- onClick(number)}
- to={`verse:${chapter.chapterNumber}:${number}`}
- smooth
- spy
- offset={-120}
- activeClass="active"
- duration={500}
- className="pointer"
- >
- {number}
-
-
- );
- }
-
- return (
- onClick(number)}>
- {number}
-
- );
- }
-
- renderMenu() {
- const { chapter } = this.props;
- const array = Array(chapter.versesCount).join().split(',');
-
- return array.map((ayah, index) => this.renderItem(ayah, index));
- }
-
- render() {
- const { className } = this.props;
-
- const title = (
-
- );
-
- return (
-
- {this.renderMenu()}
-
- );
- }
-}
-
-VersesDropdown.propTypes = {
- loadedVerses: PropTypes.instanceOf(Set).isRequired,
- chapter: customPropTypes.surahType.isRequired, // Set
- onClick: PropTypes.func.isRequired,
- isReadingMode: PropTypes.bool,
- className: PropTypes.string
-};
-
-VersesDropdown.defaultProps = {
- className: ''
-};
-
-export default VersesDropdown;
diff --git a/src/components/VersesDropdown/style.scss b/src/components/VersesDropdown/style.scss
deleted file mode 100644
index f09daeebc..000000000
--- a/src/components/VersesDropdown/style.scss
+++ /dev/null
@@ -1,9 +0,0 @@
-.dropdown{
- :global(.dropdown-menu){
- max-height: 400px;
- max-height: 60vh;
- overflow-y: scroll;
- -webkit-overflow-scrolling: touch;
- overflow-x: hidden;
- }
-}
diff --git a/src/components/Word.tsx b/src/components/Word.tsx
new file mode 100644
index 000000000..7f8d447ad
--- /dev/null
+++ b/src/components/Word.tsx
@@ -0,0 +1,232 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { Tooltip } from 'react-tippy';
+import pad from 'lodash/pad';
+import styled, { StyledFunction } from 'styled-components';
+import { buildAudioURL } from '../helpers/buildAudio';
+import { WordShape } from '../shapes';
+import { WORD_TYPES } from '../constants';
+import {
+ SetCurrentVerseKey,
+ SetCurrentWord,
+ Pause,
+ PlayCurrentWord,
+} from '../redux/actions/audioplayer';
+
+const WordGlyph = styled.span`
+ -webkit-font-smoothing: antialiased;
+`;
+
+interface WordWrapProps {
+ highlight?: boolean;
+}
+const WordWrapStyled: StyledFunction<
+ WordWrapProps & React.HTMLProps
+> =
+ styled.a;
+const WordWrap = WordWrapStyled`
+ -webkit-font-smoothing: antialiased;
+ float: right;
+ color: ${({ highlight, theme }) =>
+ highlight ? theme.brandPrimary : 'initial'};
+`;
+
+const WordText = styled.i`
+ position: absolute;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ color: transparent;
+ -webkit-user-select: text;
+ -moz-user-select: text;
+ -ms-user-select: text;
+ user-select: text;
+`;
+
+const propTypes = {
+ word: WordShape.isRequired,
+ setCurrentWord: PropTypes.func.isRequired,
+ pause: PropTypes.func.isRequired,
+ setCurrentVerseKey: PropTypes.func.isRequired,
+ playCurrentWord: PropTypes.func.isRequired,
+ tooltip: PropTypes.string.isRequired,
+ audioPosition: PropTypes.number,
+ isCurrentVersePlaying: PropTypes.bool.isRequired,
+ isSearched: PropTypes.bool,
+ useTextFont: PropTypes.bool, // tmp change to compare text and code based rendering
+};
+
+type DefaultProps = {
+ audioPosition?: number | undefined;
+ isSearched?: boolean;
+ useTextFont?: boolean;
+ currentVerse?: string | null;
+};
+
+const defaultProps: DefaultProps = {
+ isSearched: false,
+ useTextFont: false,
+ audioPosition: undefined,
+ currentVerse: null,
+};
+
+type Props = {
+ word: WordShape;
+ setCurrentWord: SetCurrentWord;
+ pause: Pause;
+ setCurrentVerseKey: SetCurrentVerseKey;
+ playCurrentWord: PlayCurrentWord;
+ tooltip: 'translation' | 'transliteration';
+ audioPosition?: number;
+ isCurrentVersePlaying: boolean;
+ isSearched?: boolean;
+ useTextFont?: boolean;
+};
+
+class Word extends Component {
+ public static propTypes = propTypes;
+ public static defaultProps = defaultProps;
+
+ timer: number | undefined = undefined;
+
+ getTooltipTitle = () => {
+ const { word, tooltip } = this.props;
+
+ let title = '';
+
+ if (word.charType === WORD_TYPES.CHAR_TYPE_END) {
+ title = `Verse ${word.verseKey.split(':')[1]}`;
+ } else if (word.charType === WORD_TYPES.CHAR_TYPE_WORD) {
+ if (word[tooltip]) {
+ title = (word[tooltip] || {}).text || '';
+ }
+ }
+
+ return title;
+ };
+
+ getLanguageName = () => {
+ const { word, tooltip } = this.props;
+ const content = word[tooltip];
+
+ if (content) {
+ return content.languageName;
+ }
+
+ return '';
+ };
+
+ handleClick = () => {
+ if (this.timer && this.timer < 300) {
+ this.handleSegmentPlay();
+ window.clearTimeout(this.timer);
+ this.timer = undefined;
+ } else {
+ this.timer = window.setTimeout(() => {
+ this.handleWordPlay();
+ window.clearTimeout(this.timer);
+ this.timer = undefined;
+ }, 300);
+ }
+ };
+
+ handleWordPlay = () => {
+ const { word } = this.props;
+
+ if (word.audio) {
+ const audio = new Audio(buildAudioURL(word.audio));
+ audio.play();
+ }
+ };
+
+ handleSegmentPlay = () => {
+ const {
+ word,
+ audioPosition,
+ isSearched,
+ setCurrentWord,
+ pause,
+ setCurrentVerseKey,
+ playCurrentWord,
+ isCurrentVersePlaying,
+ } = this.props;
+
+ if (isSearched || !word.audio) {
+ return;
+ }
+
+ if (isCurrentVersePlaying) {
+ setCurrentWord(word.code);
+ } else {
+ pause();
+ setCurrentVerseKey(word.verseKey);
+ playCurrentWord({ word, position: audioPosition });
+ }
+ };
+
+ render() {
+ const {
+ audioPosition,
+ isCurrentVersePlaying,
+ word,
+ useTextFont,
+ } = this.props;
+
+ let text = '';
+ const { verseKey } = word;
+ const className = `${useTextFont ? 'text-' : ''}${word.className} ${
+ word.charType
+ } ${word.highlight ? word.highlight : ''}`;
+
+ const id = `word-${(verseKey || '').replace(/:/, '-')}-${audioPosition}`;
+
+ if (useTextFont) {
+ if (word.charType === WORD_TYPES.CHAR_TYPE_END) {
+ text = pad(word.verseKey.split(':')[1], 3, '0');
+ } else if (word.textMadani) {
+ text = word.textMadani;
+ }
+ } else {
+ text = word.code;
+ }
+
+ const tooltipText = this.getTooltipTitle();
+ const tooltipHtml = (
+ {tooltipText}
+ );
+
+ return (
+
+
+
+
+
+
+
+
+ {word.charType === WORD_TYPES.CHAR_TYPE_WORD && (
+
+ )}
+
+ );
+ }
+}
+
+export default Word;
diff --git a/src/components/Word/index.js b/src/components/Word/index.js
deleted file mode 100644
index ed1a36e41..000000000
--- a/src/components/Word/index.js
+++ /dev/null
@@ -1,104 +0,0 @@
-import React, { Component, PropTypes } from 'react';
-import bindTooltip from 'utils/bindTooltip';
-import { zeroPad } from 'helpers/StringHelpers';
-
-/* eslint-disable no-unused-vars */
-const CHAR_TYPE_WORD = 'word';
-const CHAR_TYPE_END = 'end';
-const CHAR_TYPE_PAUSE = 'pause';
-const CHAR_TYPE_RUB = 'rub';
-const CHAR_TYPE_SAJDAH = 'sajdah';
-
-class Word extends Component {
- buildTooltip = (word, tooltip) => {
- let title;
-
- if (word.charType === CHAR_TYPE_END) {
- title = `Verse ${word.verseKey.split(':')[1]}`;
- } else if (word.charType === CHAR_TYPE_WORD) {
- title = word[tooltip].text;
- } else {
- title = '';
- }
- return title;
- }
-
- handleWordPlay = () => {
- const { word } = this.props;
-
- if (word.audio) {
- const audio = new Audio(word.audio.url); // eslint-disable-line
-
- audio.play();
- }
- }
-
- handleSegmentPlay = () => {
- const { word, currentVerse, audioActions, audioPosition, isPlaying, isSearched } = this.props;
-
- if (isSearched || !word.audio) {
- return;
- }
-
- if ((currentVerse === word.verseKey) && isPlaying) {
- audioActions.setCurrentWord(word.code);
- } else {
- audioActions.pause();
- audioActions.setAyah(word.verseKey);
- audioActions.playCurrentWord({ word, position: audioPosition });
- }
- }
-
- render() {
- const { tooltip, word, currentVerse, isPlaying, audioPosition, useTextFont } = this.props;
-
- let text;
- let spacer;
- const highlight = currentVerse === word.verseKey && isPlaying ? 'highlight' : '';
- const className = `${useTextFont ? 'text-' : ''}${word.className} ${word.charType} ${highlight} ${word.highlight ? word.highlight : ''}`;
- const id = `word-${word.verseKey.replace(/:/, '-')}-${audioPosition}`;
-
- if (useTextFont) {
- if (word.charType === CHAR_TYPE_END) {
- text = zeroPad(word.verseKey.split(':')[1], 3, 0);
- } else {
- text = word.textMadani;
- }
- } else {
- text = word.code;
- }
-
- if (word.charType === CHAR_TYPE_WORD) {
- spacer = ' ';
- }
-
- return (
-
-
-
-
- );
- }
-}
-
-Word.propTypes = {
- word: PropTypes.object.isRequired, // eslint-disable-line
- tooltip: PropTypes.string,
- audioActions: PropTypes.object.isRequired, // eslint-disable-line
- audioPosition: PropTypes.number,
- currentVerse: PropTypes.string,
- isPlaying: PropTypes.bool,
- isSearched: PropTypes.bool,
- useTextFont: PropTypes.bool // tmp change to compare text and code based rendering
-};
-
-export default Word;
diff --git a/src/components/_tests_/About.test.tsx b/src/components/_tests_/About.test.tsx
new file mode 100644
index 000000000..3f06e5e4d
--- /dev/null
+++ b/src/components/_tests_/About.test.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import About from '../About';
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/AppHelmet.test.tsx b/src/components/_tests_/AppHelmet.test.tsx
new file mode 100644
index 000000000..63a568a65
--- /dev/null
+++ b/src/components/_tests_/AppHelmet.test.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import AppHelmet from '../AppHelmet';
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/Audioplayer.test.tsx b/src/components/_tests_/Audioplayer.test.tsx
new file mode 100644
index 000000000..b6477103f
--- /dev/null
+++ b/src/components/_tests_/Audioplayer.test.tsx
@@ -0,0 +1,308 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import sampleSize from 'lodash/sampleSize';
+import Audioplayer, { DIRECTIONS } from '../Audioplayer';
+
+import { chapter } from '../../../tests/fixtures/chapters';
+import verse, { getVerse } from '../../../tests/fixtures/verse';
+
+let getProps: any;
+let getFileMock: any;
+let fileMock: any;
+
+describe(' {
+ beforeEach(() => {
+ getFileMock = (overrides = {}) => ({
+ play: jest.fn(),
+ pause: jest.fn(),
+ setAttribute: jest.fn(),
+ src: sampleSize('abcdefg', 3).join(''),
+ duration: 1000,
+ paused: true,
+ ...overrides,
+ });
+
+ getProps = (overrides = {}) => ({
+ chapter,
+ play: jest.fn(),
+ pause: jest.fn(),
+ setCurrentVerseKey: jest.fn(),
+ update: jest.fn(),
+ setRepeat: jest.fn(),
+ fetchAudio: jest.fn(() => Promise.resolve({})),
+ shouldScroll: false,
+ toggleScroll: jest.fn(),
+ isPlaying: false,
+ audioSetting: 7,
+ repeat: {},
+ files: {
+ '1:1': getFileMock(),
+ '1:2': getFileMock(),
+ },
+ verses: {
+ '1:1': verse,
+ '1:2': getVerse(1, 2),
+ '1:3': getVerse(1, 3),
+ },
+ ...overrides,
+ });
+
+ fileMock = getFileMock();
+ });
+
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+
+ describe('#componentDidMount', () => {
+ it('fetches audio', () => {
+ const props = getProps({ currentVerseKey: '1:1' });
+ const { fetchAudio } = props;
+ shallow( );
+
+ expect(fetchAudio).toBeCalled();
+ expect(fetchAudio).toBeCalledWith({
+ audio: 7,
+ chapterId: 1,
+ isCurrentVerse: false,
+ verseId: 1,
+ verseKey: '1:1',
+ });
+ });
+
+ it('fetches audio with isCurrentVerse when no currentVerseKey is present', () => {
+ const props = getProps();
+ const { fetchAudio } = props;
+ shallow( );
+
+ expect(fetchAudio).toBeCalled();
+ expect(fetchAudio).toBeCalledWith({
+ audio: 7,
+ chapterId: 1,
+ isCurrentVerse: true,
+ verseId: 1,
+ verseKey: '1:1',
+ });
+ });
+ });
+ describe('#componentDidUpdate', () => {
+ it('calls currentFile.play when isPlaying changes to true', () => {
+ const props = getProps({ currentFile: fileMock });
+ const wrapper = shallow( );
+
+ wrapper.setProps({ isPlaying: true });
+
+ expect(fileMock.play).toHaveBeenCalled();
+ });
+
+ it('calls currentFile.pause when isPlaying changes to false', () => {
+ const currentFile = getFileMock({ paused: false });
+ const props = getProps({
+ currentFile,
+ isPlaying: true,
+ });
+ const wrapper = shallow( );
+
+ wrapper.setProps({ isPlaying: false });
+
+ expect(currentFile.pause).toHaveBeenCalled();
+ });
+
+ it('fetches next audio when currentFile changes', () => {
+ const props = getProps({ currentVerseKey: '1:1', currentFile: fileMock });
+ const wrapper = shallow( );
+ const { files, fetchAudio } = props;
+
+ wrapper.setProps({ currentVerseKey: '1:2', currentFile: files['1:2'] });
+
+ expect(fetchAudio).toHaveBeenLastCalledWith({
+ audio: 7,
+ chapterId: 1,
+ verseId: 3,
+ verseKey: '1:3',
+ });
+
+ expect(fileMock.onloadeddata).toBeNull();
+ expect(fileMock.ontimeupdate).toBeNull();
+ expect(fileMock.onpause).toBeNull();
+ expect(fileMock.onplay).toBeNull();
+ expect(fileMock.onended).toBeNull();
+ expect(fileMock.onprogress).toBeNull();
+ expect(files['1:2'].onloadeddata).toBeDefined();
+ expect(files['1:2'].onpause).toBeDefined();
+ expect(files['1:2'].onplay).toBeDefined();
+ expect(files['1:2'].onended).toBeDefined();
+ });
+
+ it('fetches audio file when currentFile is not present', () => {
+ const props = getProps({ currentVerseKey: '1:1', currentFile: fileMock });
+ const wrapper = shallow( );
+ const { fetchAudio } = props;
+
+ wrapper.setProps({ currentVerseKey: '1:3' });
+
+ expect(fetchAudio).toHaveBeenLastCalledWith({
+ audio: 7,
+ chapterId: 1,
+ verseId: 3,
+ verseKey: '1:3',
+ });
+ });
+ });
+
+ describe('componentWillUnount', () => {
+ it('removes all listeners when currentFile present', () => {
+ const props = getProps({ currentVerseKey: '1:1' });
+ const wrapper = shallow( );
+
+ wrapper.setProps({ currentFile: fileMock });
+ wrapper.unmount();
+
+ expect(fileMock.pause).toHaveBeenCalled();
+ expect(fileMock.currentTime).toEqual(0);
+ expect(fileMock.onloadeddata).toBeNull();
+ expect(fileMock.ontimeupdate).toBeNull();
+ expect(fileMock.onpause).toBeNull();
+ expect(fileMock.onplay).toBeNull();
+ expect(fileMock.onended).toBeNull();
+ expect(fileMock.onprogress).toBeNull();
+ });
+ });
+
+ describe('currentFile listeners', () => {
+ it('calls update on currentFile loaded', () => {
+ const props = getProps({ currentVerseKey: '1:1', currentFile: fileMock });
+ const wrapper = shallow( );
+ const { update } = props;
+ const instance = wrapper.instance() as Audioplayer;
+
+ instance.addFileListeners(fileMock);
+
+ fileMock.onloadeddata();
+
+ expect(update).toHaveBeenCalled();
+ expect(update).toHaveBeenCalledWith({
+ duration: fileMock.duration,
+ currentTime: 0,
+ isLoading: false,
+ });
+ });
+
+ it('sets ontimeupdate listener when onplay triggered', () => {
+ const props = getProps({ currentVerseKey: '1:1', currentFile: fileMock });
+ const wrapper = shallow( );
+ const instance = wrapper.instance() as Audioplayer;
+
+ instance.addFileListeners(fileMock);
+
+ expect(fileMock.ontimeupdate).toBeUndefined();
+
+ fileMock.onplay();
+
+ expect(fileMock.ontimeupdate).toBeDefined();
+ });
+
+ it('removes ontimeupdate listener when onpause triggered', () => {
+ const props = getProps({ currentVerseKey: '1:1', currentFile: fileMock });
+ const wrapper = shallow( );
+ const instance = wrapper.instance() as Audioplayer;
+
+ instance.addFileListeners(fileMock);
+
+ fileMock.ontimeupdate = () => {};
+
+ expect(fileMock.ontimeupdate).toBeDefined();
+
+ fileMock.onpause();
+
+ expect(fileMock.ontimeupdate).toBeNull();
+ });
+
+ it('preloads next file when current file is almost over', () => {
+ const props = getProps({ currentVerseKey: '1:1', currentFile: fileMock });
+ const wrapper = shallow( );
+ const { files } = props;
+ const instance = wrapper.instance() as Audioplayer;
+ const nextFile = files['1:2'];
+
+ instance.addFileListeners(fileMock);
+
+ fileMock.currentTime = fileMock.duration;
+
+ fileMock.onplay();
+ fileMock.ontimeupdate();
+
+ expect(nextFile.setAttribute).toHaveBeenCalled();
+ });
+
+ it('calls update when time is updated on file', () => {
+ const props = getProps({ currentVerseKey: '1:1', currentFile: fileMock });
+ const wrapper = shallow( );
+ const { update } = props;
+ const instance = wrapper.instance() as Audioplayer;
+
+ instance.addFileListeners(fileMock);
+
+ fileMock.onplay();
+ fileMock.ontimeupdate();
+
+ expect(update).toHaveBeenCalled();
+ });
+
+ it('calls setCurrentVerseKey when file has ended', () => {
+ const props = getProps({ currentVerseKey: '1:1', currentFile: fileMock });
+ const wrapper = shallow( );
+ const { setCurrentVerseKey } = props;
+ const instance = wrapper.instance() as Audioplayer;
+
+ instance.addFileListeners(fileMock);
+ fileMock.readyState = 3;
+ fileMock.onended();
+
+ expect(setCurrentVerseKey).toHaveBeenCalled();
+ expect(setCurrentVerseKey).toHaveBeenCalledWith('1:2', true);
+ });
+ });
+
+ describe('handling next verse', () => {
+ it('sets the next verse', () => {
+ const props = getProps({ currentVerseKey: '1:1', currentFile: fileMock });
+ const wrapper = shallow( );
+ const { setCurrentVerseKey } = props;
+ const instance = wrapper.instance() as Audioplayer;
+
+ instance.handleVerseChange(DIRECTIONS.NEXT);
+
+ expect(setCurrentVerseKey).toHaveBeenCalled();
+ expect(setCurrentVerseKey).toHaveBeenCalledWith('1:2', true);
+ });
+ });
+
+ describe('handling previous verse', () => {
+ it('does not set the previous verse when first verse', () => {
+ const props = getProps({ currentVerseKey: '1:1', currentFile: fileMock });
+ const wrapper = shallow( );
+ const { setCurrentVerseKey } = props;
+ const instance = wrapper.instance() as Audioplayer;
+
+ instance.handleVerseChange(DIRECTIONS.PREVIOUS);
+
+ expect(setCurrentVerseKey).not.toHaveBeenCalled();
+ });
+
+ it('sets the previous verse', () => {
+ const props = getProps({
+ currentVerseKey: '1:2',
+ currentFile: fileMock,
+ });
+ const wrapper = shallow( );
+ const { setCurrentVerseKey } = props;
+ const instance = wrapper.instance() as Audioplayer;
+
+ instance.handleVerseChange(DIRECTIONS.PREVIOUS);
+
+ expect(setCurrentVerseKey).toHaveBeenCalled();
+ expect(setCurrentVerseKey).toHaveBeenCalledWith('1:1', true);
+ });
+ });
+});
diff --git a/src/components/_tests_/Bismillah.test.tsx b/src/components/_tests_/Bismillah.test.tsx
new file mode 100644
index 000000000..0f875a8f9
--- /dev/null
+++ b/src/components/_tests_/Bismillah.test.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import Bismillah from '../Bismillah';
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/ChapterIcon.test.tsx b/src/components/_tests_/ChapterIcon.test.tsx
new file mode 100644
index 000000000..6bfd57dfa
--- /dev/null
+++ b/src/components/_tests_/ChapterIcon.test.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import ChapterIcon from '../ChapterIcon';
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/ChapterInfo.test.tsx b/src/components/_tests_/ChapterInfo.test.tsx
new file mode 100644
index 000000000..a7a90cac1
--- /dev/null
+++ b/src/components/_tests_/ChapterInfo.test.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import ChapterInfo from '../ChapterInfo';
+import { chapter } from '../../../tests/fixtures/chapters';
+import { chapterInfo } from '../../../tests/fixtures/chapterInfos';
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(
+ React.isValidElement(
+
+ )
+ ).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/ChapterLink.test.tsx b/src/components/_tests_/ChapterLink.test.tsx
new file mode 100644
index 000000000..7065ce956
--- /dev/null
+++ b/src/components/_tests_/ChapterLink.test.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import ChapterLink from '../ChapterLink';
+import { chapter } from '../../../tests/fixtures/chapters';
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(
+ React.isValidElement( )
+ ).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/ChaptersList.test.tsx b/src/components/_tests_/ChaptersList.test.tsx
new file mode 100644
index 000000000..35e7b365e
--- /dev/null
+++ b/src/components/_tests_/ChaptersList.test.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import ChaptersList from '../ChaptersList';
+import { chapter } from '../../../tests/fixtures/chapters';
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(
+ React.isValidElement( )
+ ).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/ComponentLoader.test.tsx b/src/components/_tests_/ComponentLoader.test.tsx
new file mode 100644
index 000000000..564f724e8
--- /dev/null
+++ b/src/components/_tests_/ComponentLoader.test.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import ComponentLoader from '../ComponentLoader';
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(
+ React.isValidElement(
+
+ )
+ ).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/Contact.test.tsx b/src/components/_tests_/Contact.test.tsx
new file mode 100644
index 000000000..a3ed85745
--- /dev/null
+++ b/src/components/_tests_/Contact.test.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import Contact from '../Contact';
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/Donations.test.tsx b/src/components/_tests_/Donations.test.tsx
new file mode 100644
index 000000000..2af6245e6
--- /dev/null
+++ b/src/components/_tests_/Donations.test.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import Donations from '../Donations';
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/Error.test.tsx b/src/components/_tests_/Error.test.tsx
new file mode 100644
index 000000000..0e76ec295
--- /dev/null
+++ b/src/components/_tests_/Error.test.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import Error from '../Error';
+import LOCALE_KEYS from '../../locale/keys';
+
+const props = {
+ match: {
+ params: {
+ errorKey: LOCALE_KEYS.ERROR_INVALID_CHAPTER as 'ERROR_INVALID_CHAPTER',
+ },
+ },
+};
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/FontText.test.tsx b/src/components/_tests_/FontText.test.tsx
new file mode 100644
index 000000000..fa3c5376f
--- /dev/null
+++ b/src/components/_tests_/FontText.test.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import FontText from '../FontText';
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/FootNote.test.tsx b/src/components/_tests_/FootNote.test.tsx
new file mode 100644
index 000000000..d304c9438
--- /dev/null
+++ b/src/components/_tests_/FootNote.test.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import FootNote from '../FootNote';
+
+const footNote = {
+ id: 1,
+ text: 'text',
+ languageName: 'english',
+};
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/Footer.test.tsx b/src/components/_tests_/Footer.test.tsx
new file mode 100644
index 000000000..ee0855e83
--- /dev/null
+++ b/src/components/_tests_/Footer.test.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import Footer from '../Footer';
+
+describe('', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement()).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/JuzDecoration.test.tsx b/src/components/_tests_/JuzDecoration.test.tsx
new file mode 100644
index 000000000..2d0dec7f9
--- /dev/null
+++ b/src/components/_tests_/JuzDecoration.test.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import JuzDecoration from '../JuzDecoration';
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/JuzLink.test.tsx b/src/components/_tests_/JuzLink.test.tsx
new file mode 100644
index 000000000..899942e0e
--- /dev/null
+++ b/src/components/_tests_/JuzLink.test.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import JuzLink from '../JuzLink';
+import { chapter } from '../../../tests/fixtures/chapters';
+import { juz } from '../../../tests/fixtures/juzs';
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(
+ React.isValidElement( )
+ ).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/JuzMarker.test.tsx b/src/components/_tests_/JuzMarker.test.tsx
new file mode 100644
index 000000000..abfcfc42f
--- /dev/null
+++ b/src/components/_tests_/JuzMarker.test.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import JuzMarker from '../JuzMarker';
+import verse from '../../../tests/fixtures/verse';
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(
+ React.isValidElement( )
+ ).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/List.test.tsx b/src/components/_tests_/List.test.tsx
new file mode 100644
index 000000000..ebe526c1d
--- /dev/null
+++ b/src/components/_tests_/List.test.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import List from '../List';
+
+describe('
', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement(
)).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/MobileLanding.test.tsx b/src/components/_tests_/MobileLanding.test.tsx
new file mode 100644
index 000000000..4e1aa4b55
--- /dev/null
+++ b/src/components/_tests_/MobileLanding.test.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import MobileLanding from '../MobileLanding';
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/Navbar.test.tsx b/src/components/_tests_/Navbar.test.tsx
new file mode 100644
index 000000000..86cfcc1b5
--- /dev/null
+++ b/src/components/_tests_/Navbar.test.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import Navbar from '../Navbar';
+
+const props = {
+ location: {
+ pathname: '/',
+ },
+ handleSidebarToggle: jest.fn(),
+};
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/NoScript.test.tsx b/src/components/_tests_/NoScript.test.tsx
new file mode 100644
index 000000000..bead56592
--- /dev/null
+++ b/src/components/_tests_/NoScript.test.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import NoScript from '../NoScript';
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/NotFound.test.tsx b/src/components/_tests_/NotFound.test.tsx
new file mode 100644
index 000000000..449cfcd3c
--- /dev/null
+++ b/src/components/_tests_/NotFound.test.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import NotFound from '../NotFound';
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/PageBreak.test.tsx b/src/components/_tests_/PageBreak.test.tsx
new file mode 100644
index 000000000..e170f4cc2
--- /dev/null
+++ b/src/components/_tests_/PageBreak.test.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import PageBreak from '../PageBreak';
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/QuickChapters.test.tsx b/src/components/_tests_/QuickChapters.test.tsx
new file mode 100644
index 000000000..6e20af48d
--- /dev/null
+++ b/src/components/_tests_/QuickChapters.test.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import QuickChapters from '../QuickChapters';
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/RouterStatus.test.tsx b/src/components/_tests_/RouterStatus.test.tsx
new file mode 100644
index 000000000..d06fb9911
--- /dev/null
+++ b/src/components/_tests_/RouterStatus.test.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import RouterStatus from '../RouterStatus';
+
+const props = {
+ code: 400,
+ children:
,
+};
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/Share.test.tsx b/src/components/_tests_/Share.test.tsx
new file mode 100644
index 000000000..b842a4735
--- /dev/null
+++ b/src/components/_tests_/Share.test.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import Share from '../Share';
+import verse from '../../../tests/fixtures/verse';
+import { chapter } from '../../../tests/fixtures/chapters';
+
+const props = {
+ chapter,
+ verse,
+};
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/T.test.tsx b/src/components/_tests_/T.test.tsx
new file mode 100644
index 000000000..cb83ee9fe
--- /dev/null
+++ b/src/components/_tests_/T.test.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import T, { KEYS } from '../T';
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/Tafsir.test.tsx b/src/components/_tests_/Tafsir.test.tsx
new file mode 100644
index 000000000..a4fac0dbf
--- /dev/null
+++ b/src/components/_tests_/Tafsir.test.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import Tafsir from '../Tafsir';
+import verse from '../../../tests/fixtures/verse';
+
+const props = {
+ verse,
+};
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/Translation.test.tsx b/src/components/_tests_/Translation.test.tsx
new file mode 100644
index 000000000..0e3f7bdbf
--- /dev/null
+++ b/src/components/_tests_/Translation.test.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import Translation from '../Translation';
+import verse from '../../../tests/fixtures/verse';
+
+const props = {
+ translation: verse.translations[0],
+ fetchFootNote: jest.fn(),
+};
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/TranslationNode.test.tsx b/src/components/_tests_/TranslationNode.test.tsx
new file mode 100644
index 000000000..74b77582c
--- /dev/null
+++ b/src/components/_tests_/TranslationNode.test.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import TranslationNode from '../TranslationNode';
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/Verse.test.tsx b/src/components/_tests_/Verse.test.tsx
new file mode 100644
index 000000000..846fbdf7f
--- /dev/null
+++ b/src/components/_tests_/Verse.test.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import Verse from '../Verse';
+import verse from '../../../tests/fixtures/verse';
+import { chapter } from '../../../tests/fixtures/chapters';
+
+const tooltip: 'translation' = 'translation';
+
+const props = {
+ verse,
+ chapter,
+ isCurrentVersePlaying: true,
+ tooltip,
+ fetchTafsirs: jest.fn(),
+ play: jest.fn(),
+ pause: jest.fn(),
+ setCurrentVerseKey: jest.fn(),
+ isPdf: false,
+};
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/VerseCopy.test.tsx b/src/components/_tests_/VerseCopy.test.tsx
new file mode 100644
index 000000000..a8ec09faa
--- /dev/null
+++ b/src/components/_tests_/VerseCopy.test.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import VerseCopy from '../VerseCopy';
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(
+ React.isValidElement( )
+ ).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/VersesDropdown.test.tsx b/src/components/_tests_/VersesDropdown.test.tsx
new file mode 100644
index 000000000..b5faaf07f
--- /dev/null
+++ b/src/components/_tests_/VersesDropdown.test.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import VersesDropdown from '../VersesDropdown';
+import verse from '../../../tests/fixtures/verse';
+
+const props = {
+ onClick: jest.fn(),
+ isReadingMode: false,
+ verses: { '1:1': verse },
+};
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/_tests_/Word.test.tsx b/src/components/_tests_/Word.test.tsx
new file mode 100644
index 000000000..79749041a
--- /dev/null
+++ b/src/components/_tests_/Word.test.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import Word from '../Word';
+import verse from '../../../tests/fixtures/verse';
+
+const word = verse.words[0];
+const tooltip: 'translation' = 'translation';
+
+const props = {
+ word,
+ setCurrentWord: jest.fn(),
+ pause: jest.fn(),
+ setCurrentVerseKey: jest.fn(),
+ playCurrentWord: jest.fn(),
+ tooltip,
+ isCurrentVersePlaying: true,
+};
+
+describe(' ', () => {
+ it('renders valid', () => {
+ expect(React.isValidElement( )).toBeTruthy();
+ });
+});
diff --git a/src/components/audioplayer/ControlButton.tsx b/src/components/audioplayer/ControlButton.tsx
new file mode 100644
index 000000000..957a4a87f
--- /dev/null
+++ b/src/components/audioplayer/ControlButton.tsx
@@ -0,0 +1,58 @@
+import styled, { css } from 'styled-components';
+
+const playingButtonStyle = css`
+ background: ${({ theme }) => theme.brandPrimary};
+ color: #fff;
+ height: 32px;
+ width: 32px;
+ padding: 8px;
+ padding-left: 9px;
+ border-radius: 50%;
+ position: relative;
+ vertical-align: middle;
+ &:hover {
+ opacity: 0.75;
+ }
+`;
+
+const disabledStyle = css`
+ opacity: 0.5;
+ cursor: not-allowed !important;
+ pointer-events: none;
+`;
+
+const isDisabledCss = ({ disabled }: { disabled?: boolean }) =>
+ disabled && disabledStyle;
+const isPlayingCss = ({ playingButton }: { playingButton?: boolean }) =>
+ playingButton && playingButtonStyle;
+const colorCss = ({ theme, active }: { theme: $TsFixMe; active?: boolean }) =>
+ active ? theme.brandPrimary : theme.textColor;
+
+export const ControlButton = styled.button<{
+ playingButton?: boolean;
+ active?: boolean;
+}>`
+ width: 100%;
+ display: inline-block;
+ cursor: pointer;
+ padding: 0 10px;
+ border: none;
+ background-color: transparent;
+ box-shadow: none;
+ color: ${colorCss};
+ outline: none;
+ text-align: center;
+
+ &:focus,
+ &:active {
+ outline: none;
+ }
+
+ ${isPlayingCss} ${isDisabledCss}
+ i.fa {
+ color: inherit;
+ font-size: 100%;
+ }
+`;
+
+export default ControlButton;
diff --git a/src/components/audioplayer/ControlItem.tsx b/src/components/audioplayer/ControlItem.tsx
new file mode 100644
index 000000000..080ecfe99
--- /dev/null
+++ b/src/components/audioplayer/ControlItem.tsx
@@ -0,0 +1,9 @@
+import styled from 'styled-components';
+
+const ControlItem = styled.li`
+ vertical-align: middle;
+ padding-right: 20px;
+ color: #939598;
+`;
+
+export default ControlItem;
diff --git a/src/components/audioplayer/NextButton.tsx b/src/components/audioplayer/NextButton.tsx
new file mode 100644
index 000000000..1524b62e7
--- /dev/null
+++ b/src/components/audioplayer/NextButton.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import toNumber from 'lodash/toNumber';
+import ControlButton from './ControlButton';
+import { ChapterShape } from '../../shapes';
+
+const propTypes = {
+ onNextClick: PropTypes.func.isRequired,
+ currentVerseKey: PropTypes.string,
+ chapter: ChapterShape.isRequired,
+};
+
+const defaultProps = {
+ currentVerseKey: '',
+};
+
+type Props = {
+ onNextClick(): void;
+ currentVerseKey?: string;
+ chapter: ChapterShape;
+};
+
+const NextButton: React.SFC = ({
+ onNextClick,
+ chapter,
+ currentVerseKey,
+}: Props) => {
+ let isEnd = true;
+
+ if (currentVerseKey) {
+ isEnd = chapter.versesCount === toNumber(currentVerseKey.split(':')[1]);
+ }
+
+ return (
+ !isEnd && onNextClick()} disabled={isEnd}>
+
+
+ );
+};
+
+NextButton.propTypes = propTypes;
+NextButton.defaultProps = defaultProps;
+
+export default NextButton;
diff --git a/src/components/audioplayer/PlayStopButton.tsx b/src/components/audioplayer/PlayStopButton.tsx
new file mode 100644
index 000000000..8b43ee02b
--- /dev/null
+++ b/src/components/audioplayer/PlayStopButton.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Loader from 'quran-components/lib/Loader';
+import ControlButton from './ControlButton';
+
+const loaderStyle = {
+ position: 'relative',
+ overflow: 'hidden',
+ width: '32px',
+ height: '32px',
+ margin: '-8px',
+ background: '#ffffff',
+};
+
+const propTypes = {
+ isPlaying: PropTypes.bool.isRequired,
+ onPause: PropTypes.func.isRequired,
+ onPlay: PropTypes.func.isRequired,
+ currentVerseKey: PropTypes.string,
+};
+
+const defaultProps = {
+ currentVerseKey: '',
+};
+
+type Props = {
+ isPlaying: boolean;
+ onPause(): void;
+ onPlay(): void;
+ currentVerseKey?: string;
+};
+
+const PlayStopButton: React.SFC = ({
+ isPlaying,
+ onPause,
+ onPlay,
+ currentVerseKey,
+}: Props) => {
+ const icon = (
+
+ );
+
+ const loader = ;
+
+ return (
+
+ {currentVerseKey ? icon : loader}
+
+ );
+};
+
+PlayStopButton.propTypes = propTypes;
+PlayStopButton.defaultProps = defaultProps;
+
+export default PlayStopButton;
diff --git a/src/components/audioplayer/Popover.tsx b/src/components/audioplayer/Popover.tsx
new file mode 100644
index 000000000..fa237fff9
--- /dev/null
+++ b/src/components/audioplayer/Popover.tsx
@@ -0,0 +1,20 @@
+import styled from 'styled-components';
+import BootstrapPopover from 'react-bootstrap/lib/Popover';
+
+const Popover = styled(BootstrapPopover)`
+ .popover-title {
+ text-transform: uppercase;
+ color: ${({ theme }) => theme.brandPrimary};
+ padding-top: 15px;
+ padding-bottom: 15px;
+ font-size: 0.75em;
+ }
+ .popover-content {
+ text-align: center;
+ a {
+ font-size: 0.8em;
+ }
+ }
+`;
+
+export default Popover;
diff --git a/src/components/audioplayer/PreviousButton.tsx b/src/components/audioplayer/PreviousButton.tsx
new file mode 100644
index 000000000..3901a054f
--- /dev/null
+++ b/src/components/audioplayer/PreviousButton.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ControlButton from './ControlButton';
+
+const propTypes = {
+ onPreviousClick: PropTypes.func.isRequired,
+ currentVerseKey: PropTypes.string,
+ files: PropTypes.object.isRequired,
+};
+
+const defaultProps = {
+ currentVerseKey: '',
+};
+
+type Props = {
+ onPreviousClick(): void;
+ currentVerseKey?: string;
+ files: { [key: string]: HTMLAudioElement };
+};
+
+const PreviousButton: React.SFC = ({
+ onPreviousClick,
+ files,
+ currentVerseKey,
+}: Props) => {
+ const index = Object.keys(files).findIndex(id => id === currentVerseKey);
+
+ return (
+ index && onPreviousClick()} disabled={!index}>
+
+
+ );
+};
+
+PreviousButton.propTypes = propTypes;
+PreviousButton.defaultProps = defaultProps;
+
+export default PreviousButton;
diff --git a/src/components/audioplayer/RepeatDropdown.tsx b/src/components/audioplayer/RepeatDropdown.tsx
new file mode 100644
index 000000000..1fc743c8b
--- /dev/null
+++ b/src/components/audioplayer/RepeatDropdown.tsx
@@ -0,0 +1,278 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import styled, { css } from 'styled-components';
+import toNumber from 'lodash/toNumber';
+import range from 'lodash/range';
+import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
+import Nav from 'react-bootstrap/lib/Nav';
+import NavItem from 'react-bootstrap/lib/NavItem';
+import FormControl from 'react-bootstrap/lib/FormControl';
+import SwitchToggle from 'quran-components/lib/Toggle';
+import T, { KEYS } from '../T';
+
+import ControlButton from './ControlButton';
+import Popover from './Popover';
+import { ChapterShape } from '../../shapes';
+import RepeatShape from '../../shapes/RepeatShape';
+import { SetRepeat } from '../../redux/actions/audioplayer';
+
+const Pill = styled(NavItem)`
+ a {
+ padding: 10px 15px;
+ }
+`;
+
+const disabledCss = css`
+ opacity: 0.5;
+ cursor: not-allowed !important;
+ pointer-events: none;
+`;
+
+const Item = styled.div<{ disabled?: boolean }>`
+ ${({ disabled }) => (disabled ? disabledCss : '')};
+`;
+
+const propTypes = {
+ chapter: ChapterShape.isRequired,
+ repeat: RepeatShape.isRequired,
+ setRepeat: PropTypes.func.isRequired,
+ current: PropTypes.number.isRequired,
+};
+
+type Props = {
+ chapter: ChapterShape;
+ repeat: RepeatShape;
+ setRepeat: SetRepeat;
+ current: number;
+};
+
+class RepeatButton extends Component {
+ static propTypes = propTypes;
+
+ handleToggle = () => {
+ const { repeat, setRepeat, current } = this.props;
+
+ if (repeat.from) {
+ return setRepeat({});
+ }
+
+ return setRepeat({
+ from: current,
+ to: current,
+ });
+ };
+
+ handleNavChange = (nav: number) => {
+ const { setRepeat, current } = this.props;
+
+ if (nav === 1) {
+ // Should set single ayah
+ return setRepeat({
+ from: current,
+ to: current,
+ });
+ }
+
+ return setRepeat({
+ from: current,
+ to: current + 3,
+ });
+ };
+
+ // TODO: PLEASE DON'T DO renderXYZ
+ renderRangeAyahs() {
+ const { chapter, repeat, setRepeat } = this.props;
+ const array = range(chapter.versesCount);
+
+ return (
+
+
+
+ :
+
+ {
+ let to = toNumber(event.target.value) + 3;
+
+ to = to < chapter.versesCount ? to : chapter.versesCount;
+
+ setRepeat({
+ ...repeat,
+ from: toNumber(event.target.value),
+ to,
+ });
+ }}
+ >
+ {array.reduce(
+ (options, time) => {
+ if (time + 1 < chapter.versesCount) {
+ // Exclude last verse
+ options.push(
+
+ {time + 1}
+
+ );
+ }
+
+ return options;
+ },
+ [] as Array
+ )}
+
+
+ -
+
+ :
+
+
+ setRepeat({ ...repeat, to: parseInt(event.target.value, 10) })
+ }
+ >
+ {array.reduce(
+ (options, time) => {
+ if (
+ (repeat.from ? repeat.from : 1) < time + 1 &&
+ time + 1 <= chapter.versesCount
+ ) {
+ // eslint-disable-line max-len
+ options.push(
+
+ {time + 1}
+
+ );
+ }
+
+ return options;
+ },
+ [] as Array
+ )}
+
+
+
+
+ );
+ }
+
+ // TODO: PLEASE DON'T DO renderXYZ
+ renderSingleAyah() {
+ const { repeat, setRepeat, chapter } = this.props;
+ const array = range(chapter.versesCount);
+
+ return (
+
+ :
+
+ setRepeat({
+ ...repeat,
+ from: parseInt(event.target.value, 10),
+ to: parseInt(event.target.value, 10),
+ })
+ }
+ >
+ {array.map(time => (
+
+ {time + 1}
+
+ ))}
+
+
+ );
+ }
+
+ render() {
+ const { repeat, setRepeat } = this.props;
+ const times = range(10);
+
+ const popover = (
+
+
+
+
+
+
+ );
+ }
+}
+
+export default RepeatButton;
diff --git a/src/components/audioplayer/ScrollButton.tsx b/src/components/audioplayer/ScrollButton.tsx
new file mode 100644
index 000000000..bee0a6829
--- /dev/null
+++ b/src/components/audioplayer/ScrollButton.tsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
+import SwitchToggle from 'quran-components/lib/Toggle';
+import T, { KEYS } from '../T';
+import ControlButton from './ControlButton';
+import Popover from './Popover';
+
+const propTypes = {
+ shouldScroll: PropTypes.bool.isRequired,
+ onScrollToggle: PropTypes.func.isRequired,
+};
+
+type Props = {
+ shouldScroll: boolean;
+ onScrollToggle: $TsFixMe;
+};
+
+const ScrollButton: React.SFC