├── .editorconfig ├── .eslintignore ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── __tests__ ├── View-test.js └── __snapshots__ │ └── View-test.js.snap ├── assets ├── animations │ └── loadingDots.json ├── images │ ├── emptyList.svg │ ├── image-fallback.png │ ├── pin-dark@3x.png │ ├── pin-light@3x.png │ └── transparent.png └── index.js ├── babel.config.js ├── components ├── ActionSheet │ ├── ActionSheet.js │ ├── ActionSheetOption.js │ └── index.js ├── AnimatedScrollingText.js ├── Button.js ├── Card.js ├── CategoryPicker │ ├── Category.js │ ├── CategoryPicker.js │ ├── index.js │ └── shapes.js ├── DateTimePicker.js ├── Divider.js ├── DropDownMenu │ ├── DropDownMenu.js │ ├── DropDownModal.js │ └── index.js ├── EmptyListImage.js ├── EmptyStateView.js ├── FormGroup.js ├── GridRow.js ├── HorizontalPager │ ├── HorizontalPager.js │ ├── Page.js │ └── index.js ├── Icon │ ├── Icon.js │ ├── Icon.web.js │ ├── assets │ │ ├── about.svg │ │ ├── activity.svg │ │ ├── add-event.svg │ │ ├── add-friend.svg │ │ ├── add-to-cart.svg │ │ ├── add-to-favorites-off.svg │ │ ├── add-to-favorites-on.svg │ │ ├── address.svg │ │ ├── attach-media.svg │ │ ├── back.svg │ │ ├── books.svg │ │ ├── call.svg │ │ ├── cam-switch.svg │ │ ├── camera.svg │ │ ├── cart.svg │ │ ├── checkbox-off.svg │ │ ├── checkbox-on.svg │ │ ├── checkbox-rectangle-off.svg │ │ ├── checkbox-rectangle-on.svg │ │ ├── checkmark-oval.svg │ │ ├── checkmark-round.svg │ │ ├── clear-text.svg │ │ ├── clock.svg │ │ ├── close.svg │ │ ├── comment.svg │ │ ├── comments.svg │ │ ├── deals.svg │ │ ├── delete.svg │ │ ├── directions.svg │ │ ├── down-arrow.svg │ │ ├── download.svg │ │ ├── drop-down.svg │ │ ├── edit.svg │ │ ├── email.svg │ │ ├── equalizer.svg │ │ ├── error-rectangle.svg │ │ ├── error.svg │ │ ├── events.svg │ │ ├── exit-to-app.svg │ │ ├── eye-crossed.svg │ │ ├── eye.svg │ │ ├── facebook-logo.svg │ │ ├── facebook.svg │ │ ├── filters.svg │ │ ├── folder.svg │ │ ├── forward-10.svg │ │ ├── forward-30.svg │ │ ├── forward-5.svg │ │ ├── forward.svg │ │ ├── friends.svg │ │ ├── full-screen.svg │ │ ├── gallery.svg │ │ ├── garbage-can.svg │ │ ├── gift.svg │ │ ├── github.svg │ │ ├── hang-up.svg │ │ ├── history.svg │ │ ├── home.svg │ │ ├── index.js │ │ ├── instagram.svg │ │ ├── laptop.svg │ │ ├── left-arrow.svg │ │ ├── like-heart.svg │ │ ├── like.svg │ │ ├── link.svg │ │ ├── linkedin.svg │ │ ├── location-pins.svg │ │ ├── lock.svg │ │ ├── loyalty-card.svg │ │ ├── maps.svg │ │ ├── mic-off.svg │ │ ├── mic.svg │ │ ├── minus-button.svg │ │ ├── missing.svg │ │ ├── more-horizontal.svg │ │ ├── music-note.svg │ │ ├── music-video.svg │ │ ├── my-location.svg │ │ ├── news.svg │ │ ├── notifications.svg │ │ ├── page.svg │ │ ├── pause.svg │ │ ├── photo.svg │ │ ├── pin.svg │ │ ├── play.svg │ │ ├── playlist-play.svg │ │ ├── plus-button.svg │ │ ├── podcasts.svg │ │ ├── products.svg │ │ ├── queue.svg │ │ ├── radio.svg │ │ ├── radiobutton-off.svg │ │ ├── radiobutton-on.svg │ │ ├── receipt.svg │ │ ├── refresh.svg │ │ ├── repeat.svg │ │ ├── replay-10.svg │ │ ├── replay-30.svg │ │ ├── replay-5.svg │ │ ├── replay.svg │ │ ├── restaurant-menu.svg │ │ ├── restaurant.svg │ │ ├── restore.svg │ │ ├── right-arrow.svg │ │ ├── rss-feed.svg │ │ ├── rsvp.svg │ │ ├── search.svg │ │ ├── settings.svg │ │ ├── share-android.svg │ │ ├── share.svg │ │ ├── sidebar.svg │ │ ├── skip-next.svg │ │ ├── sleep.svg │ │ ├── social-wall.svg │ │ ├── speed-meter.svg │ │ ├── stamp.svg │ │ ├── stop.svg │ │ ├── take-a-photo.svg │ │ ├── tiktok.svg │ │ ├── trophy.svg │ │ ├── turn-off.svg │ │ ├── tweet.svg │ │ ├── uber.svg │ │ ├── unfriend.svg │ │ ├── up-arrow.svg │ │ ├── user-profile.svg │ │ ├── users.svg │ │ ├── video-cam-off.svg │ │ ├── video-cam.svg │ │ ├── video-chat.svg │ │ ├── video-recording.svg │ │ ├── web.svg │ │ └── wheelchair.svg │ ├── index.js │ └── services │ │ ├── icons.js │ │ └── index.js ├── Image.js ├── ImageBackground.js ├── ImageGallery.js ├── ImageGalleryOverlay.js ├── ImagePreview.js ├── InlineDropDownMenu │ ├── InlineDropDownMenu.js │ ├── InlineDropDownMenuItem.js │ └── index.js ├── InlineGallery.js ├── Lightbox.js ├── LinearGradient.js ├── ListView.js ├── LoadingContainer.js ├── LoadingIndicator.js ├── NumberInput.js ├── Overlay.js ├── PageIndicators.js ├── ProgressRings │ ├── AnimatedProgressRing.js │ ├── ProgressRing.js │ ├── ProgressRings.js │ ├── const.js │ ├── index.js │ └── svgCalculations.js ├── RNHapticFeedback │ ├── index.js │ └── index.web.js ├── Row.js ├── Screen.js ├── ScrollView │ ├── ScrollDriverProvider.js │ ├── ScrollView.js │ └── index.js ├── SearchField.js ├── ShareButton.js ├── Spinner.js ├── Switch.js ├── TabMenu │ ├── TabMenu.js │ ├── TabMenuItem.js │ ├── const.js │ └── index.js ├── Text.js ├── TextInput.js ├── Tile.js ├── ToastMessage │ ├── README.md │ ├── components │ │ ├── ActionToast.js │ │ ├── BaseToast.js │ │ ├── ErrorToast.js │ │ ├── InfoToast.js │ │ ├── SuccessToast.js │ │ ├── ToastProgressBar.js │ │ └── index.js │ ├── index.js │ └── toastMessage.js ├── Touchable.js ├── TouchableNativeFeedback.js ├── TouchableOpacity.js ├── Video │ ├── Video.js │ ├── VideoSourceReader.js │ └── index.js ├── View.js └── YearRangePicker │ ├── YearRangePicker.js │ ├── YearRangePickerButton.js │ ├── YearRangePickerModal.js │ └── index.js ├── const.js ├── fonts ├── OFL.txt ├── Rubik-Black.ttf ├── Rubik-BlackItalic.ttf ├── Rubik-Bold.ttf ├── Rubik-BoldItalic.ttf ├── Rubik-Italic.ttf ├── Rubik-Light.ttf ├── Rubik-LightItalic.ttf ├── Rubik-Medium.ttf ├── Rubik-MediumItalic.ttf ├── Rubik-Regular.ttf └── rubicon-icon-font.ttf ├── helpers ├── device-selector.js ├── index.js └── keyboard.js ├── hooks ├── index.js └── useColorAndPercentageInterpolation.js ├── html ├── Html.js ├── README.md ├── components │ ├── AttachmentRenderer.js │ ├── Gallery.js │ ├── IframeRenderer.js │ ├── Image.js │ ├── SimpleHtml.js │ └── VideoRenderer.js ├── elements │ ├── A.js │ ├── Block.js │ ├── Br.js │ ├── Img.js │ ├── Inline.js │ ├── Text.js │ ├── Video.js │ ├── Virtual.js │ └── list │ │ ├── Li.js │ │ ├── Ol.js │ │ ├── Ul.js │ │ ├── helpers │ │ ├── pickLiChildElements.js │ │ └── renderItems.js │ │ ├── index.js │ │ └── prefix │ │ ├── Bullet.js │ │ └── Number.js ├── index.js └── services │ ├── Dimensions.js │ ├── DomVisitors.js │ ├── ElementRegistry.js │ ├── HTMLElementModels.js │ ├── HtmlParser.js │ ├── getEmptyObjectKeys.js │ └── isValidVideoFormat.js ├── index.js ├── init.js ├── package-lock.json ├── package.json ├── scripts └── add-native-deps.js ├── services ├── index.js ├── platform.js ├── unavailableInWeb.js └── variableResolver.js ├── theme.js ├── web └── style │ └── Alert.scss └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | # All files 5 | [*] 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | charset = utf-8 9 | 10 | # JS files 11 | [*.js] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | # JSON files 16 | [*.json] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | # Java files 21 | [*.java] 22 | indent_style = space 23 | indent_size = 4 24 | 25 | # Objective C files 26 | [*{.h,.m,.mm}] 27 | indent_style = space 28 | indent_size = 2 29 | 30 | [*.md] 31 | trim_trailing_whitespace = false 32 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/dist/* 2 | **/node_modules/* 3 | **/server.js 4 | **/webpack.config*.js 5 | **/test-utils/setup.js 6 | **/App.test.js 7 | **/__tests__/** 8 | **/_tests_/** 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | *.tgz 4 | 5 | # IDE files and folders 6 | .vscode 7 | jsconfig.json 8 | 9 | # Mac OS X folder attributes file 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | 4 | # Ignore test files 5 | *_tests_ 6 | 7 | # Ignore local/config files 8 | .editorconfig 9 | .npmignore 10 | .gitignore 11 | 12 | # Ignore previous builds 13 | *.tgz 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | For @shoutem/ui software 4 | 5 | Copyright (c) 2016-present, Shoutem. All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the Shoutem nor the 18 | names of its contributors may be used to endorse or promote products 19 | derived from this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY 25 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 28 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shoutem UI 2 | 3 | Shoutem UI is a set of styleable components that enables you to build beautiful React Native applications for iOS and Android. All of our components are built to be both composable and [customizable](http://github.com/shoutem/theme). Each component has a predefined style that is compatible with the rest of the Shoutem UI, which makes it possible to build complex components that look great without the need to manually define complex styles. 4 | 5 | ## Install 6 | 7 | These instructions are valid for React Native 0.60.0 and higher. If you're running a lower version, please use v1.X.X. 8 | 9 | ``` 10 | $ npm install --save @shoutem/ui 11 | ``` 12 | 13 | We have a `postinstall` script which will add `@shoutem/ui`'s native dependencies to your root `package.json` in order to support autolinking and pod installation. 14 | 15 | Optional: Link the font files to your iOS and Android projects using `react-native-asset`: 16 | 17 | ``` 18 | $ npx react-native-asset node_modules/@shoutem/ui/fonts 19 | ``` 20 | 21 | Not doing this will result in dismissable red screen errors about unknown font names, as well as the default system fonts being used on iOS and Android when a Shoutem UI font is meant to be used, so we suggest running this step. 22 | 23 | ## Docs 24 | 25 | All the documentation is available on the [Developer portal](http://shoutem.github.io/docs/ui-toolkit/introduction). 26 | 27 | ## Community 28 | 29 | Join [our community](https://www.facebook.com/groups/shoutem.community/) on Facebook. Also, feel free to ask a question on Stack Overflow using ["shoutem" tag](http://stackoverflow.com/tags/shoutem). 30 | 31 | ## UI Toolkit 32 | 33 | Shoutem UI is a part of the [Shoutem UI Toolkit](https://shoutem.github.io/docs/ui-toolkit/introduction) that enables you to build professional looking React Native apps with ease. 34 | 35 | It consists of three libraries: 36 | 37 | - [@shoutem/ui](https://github.com/shoutem/ui): beautiful and customizable UI components 38 | - [@shoutem/theme](https://github.com/shoutem/theme): “CSS-way” of styling entire app 39 | - [@shoutem/animation](https://github.com/shoutem/animation): declarative way of applying ready-made animations 40 | 41 | ## License 42 | 43 | [The BSD License](https://opensource.org/licenses/BSD-3-Clause) 44 | Copyright (c) 2016-present, [Shoutem](http://shoutem.github.io) 45 | -------------------------------------------------------------------------------- /__tests__/View-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import { View } from '../components/View'; 4 | 5 | test('View renders correctly', () => { 6 | const tree = renderer.create( 7 | , 8 | ).toJSON(); 9 | 10 | expect(tree).toMatchSnapshot(); 11 | }); 12 | 13 | test('View renders children vertically with vertical styleName', () => { 14 | const tree = renderer.create( 15 | 16 | 17 | 18 | 19 | ); 20 | }); 21 | 22 | test('View centers children horizontally with h-center styleName', () => { 23 | const tree = renderer.create( 24 | 25 | 26 | 27 | 28 | ); 29 | }); 30 | 31 | test('View renders children horizontally with horizontal styleName', () => { 32 | const tree = renderer.create( 33 | 34 | 35 | 36 | 37 | ); 38 | }); 39 | 40 | test('View centers children vertically with v-center styleName', () => { 41 | const tree = renderer.create( 42 | 43 | 44 | 45 | 46 | ); 47 | }); 48 | 49 | test('View fills parent with fill-parent styleName', () => { 50 | const tree = renderer.create( 51 | 52 | 53 | 54 | ); 55 | }); 56 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/View-test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`View renders correctly 1`] = ` 4 | 15 | `; 16 | -------------------------------------------------------------------------------- /assets/images/image-fallback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/assets/images/image-fallback.png -------------------------------------------------------------------------------- /assets/images/pin-dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/assets/images/pin-dark@3x.png -------------------------------------------------------------------------------- /assets/images/pin-light@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/assets/images/pin-light@3x.png -------------------------------------------------------------------------------- /assets/images/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/assets/images/transparent.png -------------------------------------------------------------------------------- /assets/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | export const animations = { 3 | loadingDots: require('./animations/loadingDots.json'), 4 | }; 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['module:metro-react-native-babel-preset'], 5 | plugins: ['@babel/plugin-proposal-class-properties'], 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /components/ActionSheet/ActionSheet.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useRef } from 'react'; 2 | import ActionSheetNative from 'react-native-actions-sheet'; 3 | import _ from 'lodash'; 4 | import PropTypes from 'prop-types'; 5 | import { connectStyle } from '@shoutem/theme'; 6 | import { View } from '../View'; 7 | import ActionSheetOption, { optionPropType } from './ActionSheetOption'; 8 | 9 | const ActionSheet = ({ 10 | active = false, 11 | cancelOptions = undefined, 12 | confirmOptions = undefined, 13 | style, 14 | onDismiss = undefined, 15 | ...otherProps 16 | }) => { 17 | const actionSheetRef = useRef(null); 18 | 19 | const hasConfirmOptions = useMemo(() => !_.isEmpty(confirmOptions), [ 20 | confirmOptions, 21 | ]); 22 | const hasCancelOptions = useMemo(() => !_.isEmpty(cancelOptions), [ 23 | cancelOptions, 24 | ]); 25 | 26 | useEffect(() => { 27 | if (active) { 28 | actionSheetRef.current?.show(); 29 | } else { 30 | actionSheetRef.current?.hide(); 31 | } 32 | }, [active]); 33 | 34 | return ( 35 | 42 | 43 | {hasConfirmOptions && ( 44 | 45 | {_.map(confirmOptions, option => ( 46 | 51 | ))} 52 | 53 | )} 54 | {hasCancelOptions && ( 55 | 56 | 57 | {_.map(cancelOptions, option => ( 58 | 64 | ))} 65 | 66 | 67 | )} 68 | 69 | 70 | ); 71 | }; 72 | 73 | ActionSheet.propTypes = { 74 | style: PropTypes.object.isRequired, 75 | active: PropTypes.bool, 76 | cancelOptions: PropTypes.arrayOf(optionPropType), 77 | confirmOptions: PropTypes.arrayOf(optionPropType), 78 | onDismiss: PropTypes.func, 79 | }; 80 | 81 | ActionSheet.defaultProps = { 82 | active: false, 83 | cancelOptions: undefined, 84 | confirmOptions: undefined, 85 | onDismiss: undefined, 86 | }; 87 | 88 | export default connectStyle('shoutem.ui.ActionSheet')(ActionSheet); 89 | -------------------------------------------------------------------------------- /components/ActionSheet/ActionSheetOption.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connectStyle } from '@shoutem/theme'; 4 | import { isIos } from '../../services'; 5 | import { Text } from '../Text'; 6 | import { TouchableOpacity } from '../TouchableOpacity'; 7 | 8 | export const optionPropType = PropTypes.shape({ 9 | title: PropTypes.string, 10 | onPress: PropTypes.func, 11 | }); 12 | 13 | function ActionSheetOption({ style, option, cancelOption, nativeStyle }) { 14 | const { title, onPress } = option; 15 | 16 | const useIosTextColor = nativeStyle && isIos; 17 | 18 | return ( 19 | 24 | 31 | {title} 32 | 33 | 34 | ); 35 | } 36 | 37 | ActionSheetOption.propTypes = { 38 | style: PropTypes.object.isRequired, 39 | cancelOption: PropTypes.bool, 40 | nativeStyle: PropTypes.bool, 41 | option: optionPropType, 42 | }; 43 | 44 | ActionSheetOption.defaultProps = { 45 | option: undefined, 46 | cancelOption: false, 47 | nativeStyle: false, 48 | }; 49 | 50 | export default connectStyle('shoutem.ui.ActionSheetOption')(ActionSheetOption); 51 | -------------------------------------------------------------------------------- /components/ActionSheet/index.js: -------------------------------------------------------------------------------- 1 | export { default as ActionSheet } from './ActionSheet'; 2 | -------------------------------------------------------------------------------- /components/Button.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { TouchableOpacity } from 'react-native'; 3 | import { connectAnimation } from '@shoutem/animation'; 4 | import { connectStyle } from '@shoutem/theme'; 5 | 6 | class Button extends PureComponent { 7 | render() { 8 | const { style } = this.props; 9 | const { underlayColor, ...otherStyle } = style; 10 | 11 | return ( 12 | 17 | ); 18 | } 19 | } 20 | 21 | Button.propTypes = { 22 | ...TouchableOpacity.propTypes, 23 | }; 24 | 25 | const AnimatedButton = connectAnimation(Button); 26 | const StyledButton = connectStyle('shoutem.ui.Button')(AnimatedButton); 27 | export { StyledButton as Button }; 28 | -------------------------------------------------------------------------------- /components/Card.js: -------------------------------------------------------------------------------- 1 | import { View } from 'react-native'; 2 | import { connectAnimation } from '@shoutem/animation'; 3 | import { connectStyle } from '@shoutem/theme'; 4 | 5 | const AnimatedCard = connectAnimation(View); 6 | const Card = connectStyle('shoutem.ui.Card')(AnimatedCard); 7 | 8 | export { Card }; 9 | -------------------------------------------------------------------------------- /components/CategoryPicker/Category.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connectStyle } from '@shoutem/theme'; 4 | import { Text } from '../Text'; 5 | import { TouchableOpacity } from '../TouchableOpacity'; 6 | import { categoryShape } from './shapes'; 7 | 8 | function Category({ category, style, isSelected, onPress }) { 9 | const textStyle = useMemo( 10 | () => [style.category, !isSelected && style.selectedCategory], 11 | 12 | [isSelected, style.category, style.selectedCategory], 13 | ); 14 | 15 | function handlePress() { 16 | if (isSelected) { 17 | return; 18 | } 19 | 20 | if (onPress) { 21 | onPress(category); 22 | } 23 | } 24 | 25 | return ( 26 | 27 | {category.name} 28 | 29 | ); 30 | } 31 | 32 | Category.propTypes = { 33 | category: categoryShape.isRequired, 34 | isSelected: PropTypes.bool, 35 | style: PropTypes.object, 36 | onPress: PropTypes.func, 37 | }; 38 | 39 | Category.defaultProps = { 40 | style: {}, 41 | isSelected: false, 42 | onPress: undefined, 43 | }; 44 | 45 | export default React.memo(connectStyle('shoutem.ui.Category')(Category)); 46 | -------------------------------------------------------------------------------- /components/CategoryPicker/CategoryPicker.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { FlatList } from 'react-native'; 3 | import _ from 'lodash'; 4 | import PropTypes from 'prop-types'; 5 | import { connectStyle } from '@shoutem/theme'; 6 | import { View } from '../View'; 7 | import Category from './Category'; 8 | import { categoryShape } from './shapes'; 9 | 10 | export function CategoryPicker({ 11 | categories, 12 | onCategorySelected, 13 | style, 14 | selectedCategory, 15 | }) { 16 | const renderItem = useCallback( 17 | ({ item: category }) => ( 18 | 24 | ), 25 | [selectedCategory.id, onCategorySelected], 26 | ); 27 | 28 | if (_.size(categories) < 2) { 29 | return null; 30 | } 31 | 32 | return ( 33 | 34 | 42 | 43 | ); 44 | } 45 | 46 | CategoryPicker.propTypes = { 47 | categories: PropTypes.arrayOf(categoryShape), 48 | selectedCategory: categoryShape, 49 | style: PropTypes.object, 50 | onCategorySelected: PropTypes.func, 51 | }; 52 | 53 | CategoryPicker.defaultProps = { 54 | categories: [], 55 | style: {}, 56 | selectedCategory: undefined, 57 | onCategorySelected: undefined, 58 | }; 59 | 60 | export default React.memo( 61 | connectStyle('shoutem.ui.CategoryPicker')(CategoryPicker), 62 | ); 63 | -------------------------------------------------------------------------------- /components/CategoryPicker/index.js: -------------------------------------------------------------------------------- 1 | export { default as CategoryPicker } from './CategoryPicker'; 2 | -------------------------------------------------------------------------------- /components/CategoryPicker/shapes.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export const categoryShape = PropTypes.shape({ 4 | id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, 5 | name: PropTypes.string.isRequired, 6 | description: PropTypes.string, 7 | }); 8 | -------------------------------------------------------------------------------- /components/Divider.js: -------------------------------------------------------------------------------- 1 | import { View } from 'react-native'; 2 | import { connectAnimation } from '@shoutem/animation'; 3 | import { connectStyle } from '@shoutem/theme'; 4 | 5 | const AnimatedDivider = connectAnimation(View); 6 | const Divider = connectStyle('shoutem.ui.Divider')(AnimatedDivider); 7 | 8 | export { Divider }; 9 | -------------------------------------------------------------------------------- /components/DropDownMenu/index.js: -------------------------------------------------------------------------------- 1 | export { DropDownMenu } from './DropDownMenu'; 2 | export { DropDownModal } from './DropDownModal'; 3 | -------------------------------------------------------------------------------- /components/EmptyListImage.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import autoBindReact from 'auto-bind/react'; 3 | import _ from 'lodash'; 4 | import PropTypes from 'prop-types'; 5 | import { connectStyle } from '@shoutem/theme'; 6 | import { Text, Title, View } from '@shoutem/ui'; 7 | import EmptyList from '../assets/images/emptyList.svg'; 8 | 9 | class EmptyListImage extends PureComponent { 10 | constructor(props) { 11 | super(props); 12 | 13 | autoBindReact(this); 14 | } 15 | 16 | resolveImage() { 17 | const { image } = this.props; 18 | 19 | if (image && !_.isFunction(image)) { 20 | // eslint-disable-next-line no-console 21 | console.warn(`Image must be an SVG file imported as a React component.`); 22 | } else if (image && _.isFunction(image)) { 23 | return image; 24 | } 25 | 26 | return EmptyList; 27 | } 28 | 29 | render() { 30 | const { 31 | message, 32 | title, 33 | imageStyle, 34 | titleStyle, 35 | messageStyle, 36 | style, 37 | } = this.props; 38 | const EmptyStateImage = this.resolveImage(); 39 | 40 | // SVG cannot receive array of style elements on Web 41 | const resolvedImageStyle = { 42 | ...style.image, 43 | ...imageStyle, 44 | }; 45 | 46 | const resolvedTitleStyle = { 47 | ...style.title, 48 | ...titleStyle, 49 | }; 50 | 51 | const resolvedTextStyle = { 52 | ...style.message, 53 | ...messageStyle, 54 | }; 55 | 56 | return ( 57 | 58 | 59 | 60 | {title} 61 | 62 | 63 | {message} 64 | 65 | 66 | ); 67 | } 68 | } 69 | 70 | EmptyListImage.propTypes = { 71 | style: PropTypes.object.isRequired, 72 | image: PropTypes.func, 73 | imageStyle: PropTypes.object, 74 | message: PropTypes.string, 75 | messageStyle: PropTypes.object, 76 | title: PropTypes.string, 77 | titleStyle: PropTypes.object, 78 | }; 79 | 80 | EmptyListImage.defaultProps = { 81 | image: undefined, 82 | imageStyle: undefined, 83 | message: "We couldn't find anything to show...", 84 | messageStyle: undefined, 85 | title: "It's empty in here", 86 | titleStyle: undefined, 87 | }; 88 | 89 | const StyledView = connectStyle('shoutem.ui.EmptyListImage')(EmptyListImage); 90 | 91 | export { StyledView as EmptyListImage }; 92 | -------------------------------------------------------------------------------- /components/EmptyStateView.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import autoBindReact from 'auto-bind/react'; 3 | import PropTypes from 'prop-types'; 4 | import { connectStyle } from '@shoutem/theme'; 5 | import { Button } from './Button'; 6 | import { Icon } from './Icon'; 7 | import { Subtitle, Text } from './Text'; 8 | import { View } from './View'; 9 | 10 | class EmptyStateView extends PureComponent { 11 | constructor(props) { 12 | super(props); 13 | 14 | autoBindReact(this); 15 | } 16 | 17 | onRetry() { 18 | const { onRetry } = this.props; 19 | 20 | onRetry(); 21 | } 22 | 23 | renderRetryButton() { 24 | const { retryButtonTitle } = this.props; 25 | 26 | // Show retry button at the bottom only if there is an onRetry action passed 27 | return ( 28 | 29 | 32 | 33 | ); 34 | } 35 | 36 | render() { 37 | const { icon, message, onRetry, ...otherProps } = this.props; 38 | 39 | return ( 40 | 41 | 42 | 43 | 44 | 45 | {message} 46 | 47 | {onRetry && this.renderRetryButton()} 48 | 49 | ); 50 | } 51 | } 52 | 53 | EmptyStateView.propTypes = { 54 | icon: PropTypes.string, 55 | message: PropTypes.string, 56 | retryButtonTitle: PropTypes.string, 57 | onRetry: PropTypes.func, 58 | }; 59 | 60 | EmptyStateView.defaultProps = { 61 | icon: 'error', 62 | message: undefined, 63 | retryButtonTitle: 'TRY AGAIN', 64 | onRetry: undefined, 65 | }; 66 | 67 | const StyledView = connectStyle('shoutem.ui.EmptyStateView')(EmptyStateView); 68 | 69 | export { StyledView as EmptyStateView }; 70 | -------------------------------------------------------------------------------- /components/FormGroup.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { connectAnimation } from '@shoutem/animation'; 3 | import { connectStyle } from '@shoutem/theme'; 4 | import { View } from './View'; 5 | 6 | class FormGroup extends PureComponent { 7 | render() { 8 | const { children, ...otherProps } = this.props; 9 | 10 | return {children}; 11 | } 12 | } 13 | 14 | FormGroup.propTypes = { 15 | ...View.propTypes, 16 | }; 17 | 18 | FormGroup.defaultProps = { 19 | ...View.defaultProps, 20 | }; 21 | 22 | const AnimatedFormGroup = connectAnimation(FormGroup); 23 | const StyledFormGroup = connectStyle('shoutem.ui.FormGroup')(AnimatedFormGroup); 24 | 25 | export { StyledFormGroup as FormGroup }; 26 | -------------------------------------------------------------------------------- /components/HorizontalPager/Page.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { View } from '../View'; 4 | 5 | /** 6 | * A HorizontalPager page. This component is used in 7 | * HorizontalPager in order to prevent unnecessary 8 | * rendering of pages that are not currently visible. 9 | */ 10 | export class Page extends Component { 11 | shouldComponentUpdate(nextProps) { 12 | return nextProps.isActive; 13 | } 14 | 15 | render() { 16 | const { width, style, children } = this.props; 17 | 18 | return ( 19 | 20 | {children} 21 | 22 | ); 23 | } 24 | } 25 | 26 | Page.propTypes = { 27 | isActive: PropTypes.bool.isRequired, 28 | width: PropTypes.number.isRequired, 29 | children: PropTypes.node, 30 | style: PropTypes.object, 31 | }; 32 | 33 | Page.defaultProps = { 34 | children: undefined, 35 | style: {}, 36 | }; 37 | -------------------------------------------------------------------------------- /components/HorizontalPager/index.js: -------------------------------------------------------------------------------- 1 | export { HorizontalPager } from './HorizontalPager'; 2 | -------------------------------------------------------------------------------- /components/Icon/Icon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connectStyle } from '@shoutem/theme'; 4 | import { getIcon } from './services'; 5 | 6 | function Icon({ name, style, ...otherProps }) { 7 | const { color, width, height, ...otherStyle } = style; 8 | 9 | const NamedIcon = getIcon(name); 10 | 11 | if (!NamedIcon) { 12 | // eslint-disable-next-line no-console 13 | console.warn(`Icon with name '${name}' not found within the provided set`); 14 | 15 | return null; 16 | } 17 | 18 | return ( 19 | 26 | ); 27 | } 28 | 29 | Icon.propTypes = { 30 | name: PropTypes.string.isRequired, 31 | style: PropTypes.object.isRequired, 32 | }; 33 | 34 | export default connectStyle('shoutem.ui.Icon')(Icon); 35 | -------------------------------------------------------------------------------- /components/Icon/Icon.web.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connectStyle } from '@shoutem/theme'; 4 | import { View } from '../View'; 5 | import { getIcon } from './services'; 6 | 7 | function Icon({ name, style, ...otherProps }) { 8 | const { color, width, height, ...otherStyle } = style; 9 | 10 | const NamedIcon = getIcon(name); 11 | 12 | if (!NamedIcon) { 13 | // eslint-disable-next-line no-console 14 | console.warn(`Icon with name '${name}' not found within the provided set`); 15 | 16 | return null; 17 | } 18 | 19 | return ( 20 | // Sometimes, in web, svg icons are not resized as expected, unless svg is wrapped inside View. 21 | 22 | 29 | 30 | ); 31 | } 32 | 33 | Icon.propTypes = { 34 | name: PropTypes.string.isRequired, 35 | style: PropTypes.object.isRequired, 36 | }; 37 | 38 | export default connectStyle('shoutem.ui.Icon')(Icon); 39 | -------------------------------------------------------------------------------- /components/Icon/assets/about.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | about 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/activity.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | activity 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/add-event.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | add-event 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/add-friend.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | add-friend 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/add-to-cart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | add-to-cart 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/add-to-favorites-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | add-to-favorites-off 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/add-to-favorites-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | add-to-favorites-on 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/address.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | address 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/attach-media.svg: -------------------------------------------------------------------------------- 1 | 2 | attach-media 3 | 4 | 5 | -------------------------------------------------------------------------------- /components/Icon/assets/back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | back 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/books.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | books 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/call.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | call 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/cam-switch.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | cam-switch 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/Icon/assets/cart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | cart 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/checkbox-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | checkbox-off 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/checkbox-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | checkbox-on 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/checkbox-rectangle-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/Icon/assets/checkbox-rectangle-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/Icon/assets/checkmark-round.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/Icon/assets/clear-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | clear-text 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/clock.svg: -------------------------------------------------------------------------------- 1 | 2 | clock 3 | 4 | 5 | -------------------------------------------------------------------------------- /components/Icon/assets/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | close 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/comment.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | comment 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/comments.svg: -------------------------------------------------------------------------------- 1 | 2 | comments 3 | 4 | 5 | -------------------------------------------------------------------------------- /components/Icon/assets/deals.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | deals 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/Icon/assets/directions.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | directions 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/down-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | down-arrow 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/download.svg: -------------------------------------------------------------------------------- 1 | 2 | download 3 | 4 | 5 | -------------------------------------------------------------------------------- /components/Icon/assets/drop-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | drop-down 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | edit 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/email.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | email 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/equalizer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | equalizer 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/error-rectangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/Icon/assets/error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | error 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/events.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | events 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/exit-to-app.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | exit-to-app 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/eye-crossed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | eye-crossed 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/eye.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | eye 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/facebook-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | facebook-logo 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | facebook 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/filters.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/Icon/assets/folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | folder 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/forward-10.svg: -------------------------------------------------------------------------------- 1 | 2 | forward-10 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/forward-30.svg: -------------------------------------------------------------------------------- 1 | 2 | forward-30 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /components/Icon/assets/forward-5.svg: -------------------------------------------------------------------------------- 1 | 2 | forward-5 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/forward.svg: -------------------------------------------------------------------------------- 1 | 2 | forward 3 | 4 | 5 | -------------------------------------------------------------------------------- /components/Icon/assets/friends.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | friends 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/full-screen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/Icon/assets/gallery.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/Icon/assets/garbage-can.svg: -------------------------------------------------------------------------------- 1 | 2 | garbage-can 3 | 4 | 5 | -------------------------------------------------------------------------------- /components/Icon/assets/gift.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | gift 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /components/Icon/assets/hang-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | hang-up 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/history.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | history 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | home 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/instagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | instagram 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/laptop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | laptop 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/left-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | left-arrow 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/like-heart.svg: -------------------------------------------------------------------------------- 1 | 2 | like-heart 3 | 4 | 5 | -------------------------------------------------------------------------------- /components/Icon/assets/like.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | like 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | link 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/linkedin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | linkedin 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/location-pins.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/Icon/assets/lock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | lock 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/loyalty-card.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | loyalty-card 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/maps.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | maps 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/mic-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | mic-off 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/mic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | mic 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/minus-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | minus-button 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/missing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | missing 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/more-horizontal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | more-horizontal 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/music-note.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/Icon/assets/music-video.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | music-video 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/my-location.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | my-location 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/news.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | news 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/notifications.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | notifications 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/page.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | page 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | pause 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/photo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | photo 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/pin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | pin 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | play 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/playlist-play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/Icon/assets/plus-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | plus-button 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/podcasts.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | podcasts 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/products.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | products 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/queue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/Icon/assets/radio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | radio 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/radiobutton-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | radiobutton-off 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/radiobutton-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | radiobutton-on 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/receipt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | receipt 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | refresh 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/repeat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/Icon/assets/replay-10.svg: -------------------------------------------------------------------------------- 1 | 2 | replay-10 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/replay-30.svg: -------------------------------------------------------------------------------- 1 | 2 | replay-30 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /components/Icon/assets/replay-5.svg: -------------------------------------------------------------------------------- 1 | 2 | replay-5 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/replay.svg: -------------------------------------------------------------------------------- 1 | 2 | replay 3 | 4 | 5 | -------------------------------------------------------------------------------- /components/Icon/assets/restaurant-menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | restaurant-menu 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/restaurant.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | restaurant 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/restore.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/Icon/assets/right-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | right-arrow 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/rss-feed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | rss-feed 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/rsvp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | rsvp 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | search 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | settings 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/share-android.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | share-android 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/share.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | share 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/sidebar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | sidebar 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/skip-next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/Icon/assets/social-wall.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | social-wall 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/speed-meter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/Icon/assets/stamp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | stamp 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | stop 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/take-a-photo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | take-a-photo 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /components/Icon/assets/tiktok.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/Icon/assets/trophy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | trophy 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/turn-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | turn-off 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/tweet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | tweet 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/uber.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | uber 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/unfriend.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | unfriend 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/up-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | up-arrow 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/user-profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | user-profile 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/users.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | users 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/video-cam-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | video-cam-off 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/video-cam.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | video-cam 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/video-chat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | video-chat 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/video-recording.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/Icon/assets/web.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | web 4 | 5 | 6 | -------------------------------------------------------------------------------- /components/Icon/assets/wheelchair.svg: -------------------------------------------------------------------------------- 1 | 2 | wheelchair 3 | 4 | 5 | -------------------------------------------------------------------------------- /components/Icon/index.js: -------------------------------------------------------------------------------- 1 | export { default as Icon } from './Icon'; 2 | export { registerIcons } from './services'; 3 | -------------------------------------------------------------------------------- /components/Icon/services/icons.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { defaultConfig } from '../assets'; 3 | 4 | const Icons = {}; 5 | 6 | function addNewIcon(config) { 7 | const hasName = _.has(config, 'name'); 8 | const hasIcon = _.has(config, 'icon'); 9 | 10 | if (!hasName || !hasIcon) { 11 | // eslint-disable-next-line no-console 12 | console.warn(`"name" and "icon" keys are required in icon config.`); 13 | return; 14 | } 15 | 16 | const { name, icon } = config; 17 | 18 | if (_.hasIn(Icons, name)) { 19 | // eslint-disable-next-line no-console 20 | console.warn( 21 | `Icon with name "${name}" already exists. Please try using another name.`, 22 | ); 23 | return; 24 | } 25 | 26 | if (!_.isString(name)) { 27 | // eslint-disable-next-line no-console 28 | console.warn(`Icon name must be a string.`); 29 | return; 30 | } 31 | 32 | if (!_.isFunction(icon)) { 33 | // eslint-disable-next-line no-console 34 | console.warn(`Icon must be an SVG file imported as a React component.`); 35 | return; 36 | } 37 | 38 | try { 39 | Icons[name] = icon; 40 | } catch (error) { 41 | // eslint-disable-next-line no-console 42 | console.warn(`Icon "${name}" could not be added: ${error.message}`); 43 | } 44 | } 45 | 46 | export function registerIcons(config) { 47 | const isArray = _.isArray(config); 48 | const isObject = _.isObject(config); 49 | 50 | if (!isObject) { 51 | // eslint-disable-next-line no-console 52 | console.warn('Icon config must be an object or an array of objects.'); 53 | return; 54 | } 55 | 56 | if (isArray) { 57 | _.forEach(config, element => { 58 | addNewIcon(element); 59 | }); 60 | } else { 61 | addNewIcon(config); 62 | } 63 | } 64 | 65 | registerIcons(defaultConfig); 66 | 67 | export function getIcon(name) { 68 | return Icons[name]; 69 | } 70 | -------------------------------------------------------------------------------- /components/Icon/services/index.js: -------------------------------------------------------------------------------- 1 | export { getIcon, registerIcons } from './icons'; 2 | -------------------------------------------------------------------------------- /components/InlineDropDownMenu/InlineDropDownMenuItem.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { Animated, Dimensions, Pressable } from 'react-native'; 3 | import autoBindReact from 'auto-bind/react'; 4 | import PropTypes from 'prop-types'; 5 | import { connectStyle } from '@shoutem/theme'; 6 | import { Text } from '../Text'; 7 | 8 | const window = Dimensions.get('window'); 9 | const AnimatedPressable = Animated.createAnimatedComponent(Pressable); 10 | 11 | class InlineDropDownMenuItem extends PureComponent { 12 | constructor(props) { 13 | super(props); 14 | 15 | autoBindReact(this); 16 | 17 | this.animatedValue = new Animated.Value(0); 18 | } 19 | 20 | componentDidMount() { 21 | const { index } = this.props; 22 | 23 | Animated.timing(this.animatedValue, { 24 | toValue: 1, 25 | useNativeDriver: true, 26 | duration: 300 + index * 15, 27 | }).start(); 28 | } 29 | 30 | handlePress() { 31 | const { onItemPressed, item } = this.props; 32 | 33 | if (onItemPressed) { 34 | onItemPressed(item); 35 | } 36 | } 37 | 38 | render() { 39 | const { isSelected, selectedDescriptor, item, style } = this.props; 40 | 41 | const resolvedText = isSelected 42 | ? `${item.title} (${selectedDescriptor})` 43 | : item.title; 44 | const textStyle = isSelected ? 'muted' : ''; 45 | 46 | return ( 47 | 64 | {resolvedText} 65 | 66 | ); 67 | } 68 | } 69 | 70 | InlineDropDownMenuItem.propTypes = { 71 | style: PropTypes.isRequired, 72 | index: PropTypes.number, 73 | isSelected: PropTypes.bool, 74 | item: PropTypes.object, 75 | selectedDescriptor: PropTypes.string, 76 | onItemPressed: PropTypes.func, 77 | }; 78 | 79 | InlineDropDownMenuItem.defaultProps = { 80 | index: undefined, 81 | isSelected: false, 82 | item: undefined, 83 | selectedDescriptor: undefined, 84 | onItemPressed: undefined, 85 | }; 86 | 87 | const StyledComponent = connectStyle('shoutem.ui.InlineDropDownMenuItem')( 88 | InlineDropDownMenuItem, 89 | ); 90 | 91 | export { StyledComponent as InlineDropDownMenuItem }; 92 | -------------------------------------------------------------------------------- /components/InlineDropDownMenu/index.js: -------------------------------------------------------------------------------- 1 | export { InlineDropDownMenu } from './InlineDropDownMenu'; 2 | export { InlineDropDownMenuItem } from './InlineDropDownMenuItem'; 3 | -------------------------------------------------------------------------------- /components/Lightbox.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import RNLightbox from 'react-native-lightbox-v2'; 3 | import { connectAnimation } from '@shoutem/animation'; 4 | import { connectStyle } from '@shoutem/theme'; 5 | 6 | // Exporting as class because Animated.createAnimatedComponent 7 | // doesn't support stateless functional components 8 | class Lightbox extends PureComponent { 9 | render() { 10 | return ; 11 | } 12 | } 13 | 14 | const AnimatedLightbox = connectAnimation(Lightbox); 15 | const StyledLightbox = connectStyle('shoutem.ui.Lightbox')(AnimatedLightbox); 16 | 17 | export { StyledLightbox as Lightbox }; 18 | -------------------------------------------------------------------------------- /components/LinearGradient.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import RNLinearGradient from 'react-native-linear-gradient'; 3 | import _ from 'lodash'; 4 | import PropTypes from 'prop-types'; 5 | import { connectAnimation } from '@shoutem/animation'; 6 | import { connectStyle } from '@shoutem/theme'; 7 | 8 | const RNLinearGradientPropsKeys = ['start', 'end', 'colors', 'locations']; 9 | 10 | class LinearGradient extends PureComponent { 11 | render() { 12 | const { children, style } = this.props; 13 | 14 | const styleWithOmissions = _.omit(style, RNLinearGradientPropsKeys); 15 | const linearGradientProps = { 16 | ...this.props, 17 | ..._.pick(style, RNLinearGradientPropsKeys), 18 | }; 19 | 20 | return ( 21 | 22 | {children} 23 | 24 | ); 25 | } 26 | } 27 | 28 | LinearGradient.propTypes = { 29 | style: PropTypes.object.isRequired, 30 | children: PropTypes.node, 31 | }; 32 | 33 | LinearGradient.defaultProps = { 34 | children: undefined, 35 | }; 36 | 37 | const AnimatedLinearGradient = connectAnimation(LinearGradient); 38 | const StyledLinearGradient = connectStyle('shoutem.ui.LinearGradient')( 39 | AnimatedLinearGradient, 40 | ); 41 | 42 | export { StyledLinearGradient as LinearGradient }; 43 | -------------------------------------------------------------------------------- /components/LoadingContainer.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState } from 'react'; 2 | import LottieView from 'lottie-react-native'; 3 | import PropTypes from 'prop-types'; 4 | import { connectStyle } from '@shoutem/theme'; 5 | import { View } from '@shoutem/ui'; 6 | import { animations } from '../assets'; 7 | 8 | function LoadingContainer({ 9 | animation, 10 | children, 11 | loading, 12 | animationScale, 13 | style, 14 | }) { 15 | const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); 16 | 17 | function onLayoutChange(event) { 18 | const { width, height } = event.nativeEvent.layout; 19 | 20 | if (width === 0 && height === 0) { 21 | return; 22 | } 23 | 24 | setDimensions({ width, height }); 25 | } 26 | 27 | const lottieViewDimensions = useMemo( 28 | () => ({ 29 | width: dimensions.width * animationScale, 30 | height: dimensions.height * animationScale, 31 | }), 32 | [animationScale, dimensions], 33 | ); 34 | 35 | if (!children) { 36 | return null; 37 | } 38 | 39 | return ( 40 | 41 | {!loading && {children}} 42 | {loading && ( 43 | 44 | 51 | 52 | )} 53 | 54 | ); 55 | } 56 | 57 | LoadingContainer.propTypes = { 58 | style: PropTypes.object.isRequired, 59 | animation: PropTypes.object, 60 | animationScale: PropTypes.number, 61 | children: PropTypes.oneOfType([ 62 | PropTypes.object, 63 | PropTypes.func, 64 | PropTypes.node, 65 | ]), 66 | loading: PropTypes.bool, 67 | }; 68 | 69 | LoadingContainer.defaultProps = { 70 | animationScale: 1, 71 | children: undefined, 72 | animation: animations.loadingDots, 73 | loading: false, 74 | }; 75 | 76 | const StyledLoadingContainer = connectStyle('shoutem.ui.LoadingContainer')( 77 | LoadingContainer, 78 | ); 79 | export { StyledLoadingContainer as LoadingContainer }; 80 | -------------------------------------------------------------------------------- /components/LoadingIndicator.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { connectStyle } from '@shoutem/theme'; 3 | import { Spinner } from './Spinner'; 4 | import { View } from './View'; 5 | 6 | /** 7 | * Renders a loading indicator (spinner) that fits into available space (container) 8 | */ 9 | class LoadingIndicator extends PureComponent { 10 | render() { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | } 20 | 21 | const StyledLoadingIndicator = connectStyle('shoutem.ui.LoadingIndicator')( 22 | LoadingIndicator, 23 | ); 24 | 25 | export { StyledLoadingIndicator as LoadingIndicator }; 26 | -------------------------------------------------------------------------------- /components/Overlay.js: -------------------------------------------------------------------------------- 1 | import { View } from 'react-native'; 2 | import { connectAnimation } from '@shoutem/animation'; 3 | import { connectStyle } from '@shoutem/theme'; 4 | 5 | const AnimatedOverlay = connectAnimation(View); 6 | const Overlay = connectStyle('shoutem.ui.Overlay')(AnimatedOverlay); 7 | 8 | export { Overlay }; 9 | -------------------------------------------------------------------------------- /components/ProgressRings/AnimatedProgressRing.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/require-default-props */ 2 | import React from 'react'; 3 | import _ from 'lodash'; 4 | import PropTypes from 'prop-types'; 5 | import { DEFAULT_PROGRESS_COLORS } from '../../const'; 6 | import { useColorAndPercentageInterpolation } from '../../hooks'; 7 | import { PROGRESS_RING_DEFAULT_PROPS } from './const'; 8 | import ProgressRing from './ProgressRing'; 9 | 10 | const AnimatedProgressRing = props => { 11 | const { 12 | progressPercentage = PROGRESS_RING_DEFAULT_PROPS.progressPercentage, 13 | progressColors = DEFAULT_PROGRESS_COLORS, 14 | animationConfig = { 15 | toValue: progressPercentage, 16 | duration: 1000 * (progressPercentage / 100), // 100% will animate for 1s, 50% for 0.5s etc. 17 | useNativeDriver: true, 18 | }, 19 | } = props; 20 | 21 | const { 22 | interpolatedColor, 23 | interpolatedPercentage, 24 | } = useColorAndPercentageInterpolation( 25 | progressColors, 26 | progressPercentage, 27 | animationConfig, 28 | ); 29 | 30 | return ( 31 | 36 | ); 37 | }; 38 | 39 | AnimatedProgressRing.propTypes = { 40 | ..._.omit(ProgressRing.propTypes, ['color']), 41 | progressColors: PropTypes.arrayOf(PropTypes.string), 42 | }; 43 | 44 | export default AnimatedProgressRing; 45 | -------------------------------------------------------------------------------- /components/ProgressRings/ProgressRings.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { View } from 'react-native'; 3 | import { G, Svg } from 'react-native-svg'; 4 | import _ from 'lodash'; 5 | import PropTypes from 'prop-types'; 6 | import { connectStyle } from '@shoutem/theme'; 7 | import AnimatedProgressRing from './AnimatedProgressRing'; 8 | import ProgressRing from './ProgressRing'; 9 | 10 | /** 11 | * Component rendering given number of rings, which are filled depending on given 12 | * percentage values. 13 | * It is possible to either specify definite color for each ring by passing ring.color prop, 14 | * or give and array of ring.progressColors and component will then interpolate between given 15 | * colors, based on given percentage value. 16 | */ 17 | const ProgressRings = ({ rings, size, rotation, children, style }) => { 18 | const renderRings = useMemo( 19 | () => 20 | rings.map((ring, index) => { 21 | const ResolvedRingComponent = ring.progressColors 22 | ? AnimatedProgressRing 23 | : ProgressRing; 24 | 25 | return ( 26 | 32 | ); 33 | }), 34 | [rings, size], 35 | ); 36 | 37 | return ( 38 | 39 | 40 | 41 | {renderRings} 42 | 43 | 44 | {children && {children}} 45 | 46 | ); 47 | }; 48 | 49 | ProgressRings.propTypes = { 50 | rings: PropTypes.arrayOf( 51 | // Omitting color to stop errors, but implementation has to define either color or progressColors, 52 | // depending on which component they're using - ProgressRing or AnimatedProgressRing, respectively. 53 | PropTypes.shape({ ..._.omit(ProgressRing.propTypes, ['index', 'color']) }), 54 | ).isRequired, 55 | children: PropTypes.func, 56 | rotation: PropTypes.number, 57 | size: PropTypes.number, 58 | style: PropTypes.object, 59 | }; 60 | 61 | ProgressRings.defaultProps = { 62 | children: undefined, 63 | size: 80, 64 | rotation: 0, 65 | style: {}, 66 | }; 67 | 68 | export default connectStyle('shoutem.ui.ProgressRings')(ProgressRings); 69 | -------------------------------------------------------------------------------- /components/ProgressRings/const.js: -------------------------------------------------------------------------------- 1 | export const PROGRESS_RING_DEFAULT_PROPS = { 2 | progressLineWidth: 10, 3 | backgroundLineWidth: 10, 4 | progressLineCap: 'round', 5 | backgroundLineCap: 'round', 6 | progressPercentage: 0.1, // 0.1 so that it shows tiny fill indicator, indicating 0%, better UX/UI than empty. 7 | arcSweepAngle: 360, 8 | size: 80, 9 | color: '#000', 10 | }; 11 | -------------------------------------------------------------------------------- /components/ProgressRings/index.js: -------------------------------------------------------------------------------- 1 | export { default as AnimatedProgressRing } from './AnimatedProgressRing'; 2 | export * from './const'; 3 | export { default as ProgressRing } from './ProgressRing'; 4 | export { default as ProgressRings } from './ProgressRings'; 5 | -------------------------------------------------------------------------------- /components/ProgressRings/svgCalculations.js: -------------------------------------------------------------------------------- 1 | const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => { 2 | const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0; 3 | return { 4 | x: centerX + radius * Math.cos(angleInRadians), 5 | y: centerY + radius * Math.sin(angleInRadians), 6 | }; 7 | }; 8 | 9 | export const circlePath = (x, y, radius, startAngle, endAngle) => { 10 | const start = polarToCartesian(x, y, radius, endAngle * 0.9999999); 11 | const end = polarToCartesian(x, y, radius, startAngle); 12 | const largeArcFlag = endAngle - startAngle <= 180 ? '0' : '1'; 13 | return [ 14 | 'M', 15 | start.x, 16 | start.y, 17 | 'A', 18 | radius, 19 | radius, 20 | 0, 21 | largeArcFlag, 22 | 0, 23 | end.x, 24 | end.y, 25 | ].join(' '); 26 | }; 27 | -------------------------------------------------------------------------------- /components/RNHapticFeedback/index.js: -------------------------------------------------------------------------------- 1 | export { default as ReactNativeHapticFeedback } from 'react-native-haptic-feedback'; 2 | -------------------------------------------------------------------------------- /components/RNHapticFeedback/index.web.js: -------------------------------------------------------------------------------- 1 | const ReactNativeHapticFeedback = { 2 | trigger: () => {}, 3 | }; 4 | 5 | export { ReactNativeHapticFeedback }; 6 | -------------------------------------------------------------------------------- /components/Row.js: -------------------------------------------------------------------------------- 1 | import { View } from 'react-native'; 2 | import { connectAnimation } from '@shoutem/animation'; 3 | import { connectStyle } from '@shoutem/theme'; 4 | 5 | const AnimatedRow = connectAnimation(View); 6 | const Row = connectStyle('shoutem.ui.Row')(AnimatedRow); 7 | 8 | export { Row }; 9 | -------------------------------------------------------------------------------- /components/Screen.js: -------------------------------------------------------------------------------- 1 | import { View } from 'react-native'; 2 | import { connectAnimation } from '@shoutem/animation'; 3 | import { connectStyle } from '@shoutem/theme'; 4 | 5 | const AnimatedScreen = connectAnimation(View); 6 | const Screen = connectStyle('shoutem.ui.Screen')(AnimatedScreen); 7 | 8 | export { Screen }; 9 | -------------------------------------------------------------------------------- /components/ScrollView/index.js: -------------------------------------------------------------------------------- 1 | export { ScrollView } from './ScrollView'; 2 | -------------------------------------------------------------------------------- /components/SearchField.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-multi-comp */ 2 | import React, { useEffect, useState } from 'react'; 3 | import { ActivityIndicator } from 'react-native'; 4 | import PropTypes from 'prop-types'; 5 | import { connectAnimation } from '@shoutem/animation'; 6 | import { connectStyle } from '@shoutem/theme'; 7 | import { Button } from './Button'; 8 | import { Icon } from './Icon'; 9 | import { TextInput } from './TextInput'; 10 | import { View } from './View'; 11 | 12 | const ClearButton = ({ style, onPress }) => ( 13 | 16 | ); 17 | 18 | ClearButton.propTypes = { 19 | style: PropTypes.object.isRequired, 20 | onPress: PropTypes.func.isRequired, 21 | }; 22 | 23 | /** 24 | * A component that allows the user to enter a search query. 25 | * It has a search icon, placeholder and a button that clears the current query. 26 | * 27 | */ 28 | const SearchField = ({ onChangeText, loading, style, ...otherProps }) => { 29 | const [text, setText] = useState(otherProps.defaultValue); 30 | 31 | useEffect(() => { 32 | onChangeText(text); 33 | // eslint-disable-next-line react-hooks/exhaustive-deps 34 | }, [text]); 35 | 36 | return ( 37 | 41 | 42 | 50 | {!!text && !loading && ( 51 | setText('')} style={style} /> 52 | )} 53 | {loading && } 54 | 55 | ); 56 | }; 57 | 58 | SearchField.propTypes = { 59 | style: PropTypes.object.isRequired, 60 | defaultValue: PropTypes.string, 61 | loading: PropTypes.bool, 62 | onChangeText: PropTypes.func, 63 | }; 64 | 65 | SearchField.defaultProps = { 66 | loading: false, 67 | onChangeText: undefined, 68 | defaultValue: '', 69 | }; 70 | 71 | const AnimatedSearchField = connectAnimation(SearchField); 72 | const StyledSearchField = connectStyle('shoutem.ui.SearchField')( 73 | AnimatedSearchField, 74 | ); 75 | 76 | export { StyledSearchField as SearchField }; 77 | -------------------------------------------------------------------------------- /components/ShareButton.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { Platform, Share } from 'react-native'; 3 | import autoBindReact from 'auto-bind/react'; 4 | import PropTypes from 'prop-types'; 5 | import { connectStyle } from '@shoutem/theme'; 6 | import { unavailableInWeb } from '../services'; 7 | import { Button } from './Button'; 8 | import { Icon } from './Icon'; 9 | 10 | /** 11 | * The ShareButton is a virtual component that wraps a button with a share icon. 12 | * It puts the sharing logic in one place. It's used in the navigation bar in the toolkit, 13 | * but it can be reused anywhere. 14 | * 15 | * It should have the style of its underlying button. That's why it's not connected to style 16 | * or animation. 17 | */ 18 | class ShareButton extends PureComponent { 19 | constructor(props) { 20 | super(props); 21 | 22 | autoBindReact(this); 23 | } 24 | 25 | onShare() { 26 | const { title, message, url } = this.props; 27 | 28 | return Share.share({ 29 | title, 30 | // URL property isn't supported on Android, so we are 31 | // including it as the message for now. 32 | message: Platform.OS === 'android' ? url : message, 33 | url, 34 | }); 35 | } 36 | 37 | render() { 38 | const { animationName, iconProps, url, ...otherProps } = this.props; 39 | 40 | if (!url) { 41 | return null; 42 | } 43 | 44 | return ( 45 | 52 | ); 53 | } 54 | } 55 | 56 | ShareButton.propTypes = { 57 | // Animation name for share icon 58 | animationName: PropTypes.string, 59 | // Additional props for Icon component 60 | iconProps: PropTypes.object, 61 | // Message to share 62 | message: PropTypes.string, 63 | // Title 64 | title: PropTypes.string, 65 | // Url to share 66 | url: PropTypes.string, 67 | }; 68 | 69 | ShareButton.defaultProps = { 70 | animationName: undefined, 71 | iconProps: undefined, 72 | message: undefined, 73 | title: undefined, 74 | url: undefined, 75 | }; 76 | 77 | const StyledShareButton = connectStyle( 78 | 'shoutem.ui.ShareButton', 79 | undefined, 80 | undefined, 81 | { virtual: true }, 82 | )(ShareButton); 83 | 84 | export { StyledShareButton as ShareButton }; 85 | -------------------------------------------------------------------------------- /components/Spinner.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { ActivityIndicator } from 'react-native'; 3 | import PropTypes from 'prop-types'; 4 | import { connectStyle } from '@shoutem/theme'; 5 | 6 | class Spinner extends PureComponent { 7 | render() { 8 | const { style } = this.props; 9 | const { color, size, ...otherStyle } = style; 10 | 11 | return ( 12 | 18 | ); 19 | } 20 | } 21 | 22 | Spinner.propTypes = { 23 | style: PropTypes.object.isRequired, 24 | }; 25 | 26 | const StyledSpinner = connectStyle('shoutem.ui.Spinner', { 27 | size: 'small', 28 | })(Spinner); 29 | 30 | export { StyledSpinner as Spinner }; 31 | -------------------------------------------------------------------------------- /components/Switch.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch as RNSwitch } from 'react-native'; 3 | import PropTypes from 'prop-types'; 4 | import { connectStyle } from '@shoutem/theme'; 5 | import { isWeb } from '../services/platform'; 6 | import { View } from './View'; 7 | 8 | const Switch = ({ value, onValueChange, style }) => { 9 | const webColors = { 10 | trackColor: style.track?.false, 11 | activeThumbColor: style.thumb, 12 | activeTrackColor: style.track.true, 13 | }; 14 | 15 | return ( 16 | 17 | 25 | 26 | ); 27 | }; 28 | 29 | Switch.propTypes = { 30 | onValueChange: PropTypes.func.isRequired, 31 | style: PropTypes.object, 32 | value: PropTypes.bool, 33 | }; 34 | 35 | Switch.defaultProps = { 36 | value: false, 37 | style: {}, 38 | }; 39 | 40 | const StyledSwitch = connectStyle('shoutem.ui.Switch')(Switch); 41 | 42 | export { StyledSwitch as Switch }; 43 | -------------------------------------------------------------------------------- /components/TabMenu/TabMenuItem.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { LayoutAnimation } from 'react-native'; 3 | import autoBindReact from 'auto-bind/react'; 4 | import PropTypes from 'prop-types'; 5 | import { connectStyle } from '@shoutem/theme'; 6 | import { Text } from '../Text'; 7 | import { TouchableOpacity } from '../TouchableOpacity'; 8 | import { View } from '../View'; 9 | import { optionShape } from './const'; 10 | 11 | class TabMenuItem extends PureComponent { 12 | constructor(props) { 13 | super(props); 14 | 15 | autoBindReact(this); 16 | 17 | this.state = { 18 | baseWidth: 0, 19 | }; 20 | } 21 | 22 | handleItemPressed() { 23 | const { onItemPressed, item } = this.props; 24 | 25 | onItemPressed(item); 26 | } 27 | 28 | handleTextLayout({ 29 | nativeEvent: { 30 | layout: { width }, 31 | }, 32 | }) { 33 | LayoutAnimation.easeInEaseOut(); 34 | this.setState({ baseWidth: width }); 35 | } 36 | 37 | handleContainerLayout({ 38 | nativeEvent: { 39 | layout: { width }, 40 | }, 41 | }) { 42 | const { onLayoutMeasured } = this.props; 43 | 44 | onLayoutMeasured(width); 45 | } 46 | 47 | render() { 48 | const { style, isSelected, item } = this.props; 49 | const { baseWidth } = this.state; 50 | 51 | return ( 52 | 57 | 61 | {item.title} 62 | 63 | {isSelected && } 64 | 65 | ); 66 | } 67 | } 68 | 69 | TabMenuItem.propTypes = { 70 | style: PropTypes.object.isRequired, 71 | onLayoutMeasured: PropTypes.func.isRequired, 72 | isSelected: PropTypes.bool, 73 | item: optionShape, 74 | onItemPressed: PropTypes.func, 75 | }; 76 | 77 | TabMenuItem.defaultProps = { 78 | isSelected: false, 79 | item: undefined, 80 | onItemPressed: undefined, 81 | }; 82 | 83 | const StyledTabMenuItem = connectStyle('shoutem.ui.TabMenuItem')(TabMenuItem); 84 | 85 | export { StyledTabMenuItem as TabMenuItem }; 86 | -------------------------------------------------------------------------------- /components/TabMenu/const.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export const optionShape = PropTypes.shape({ 4 | title: PropTypes.string, 5 | value: PropTypes.any, 6 | }); 7 | -------------------------------------------------------------------------------- /components/TabMenu/index.js: -------------------------------------------------------------------------------- 1 | export { TabMenu } from './TabMenu'; 2 | -------------------------------------------------------------------------------- /components/Text.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { Text as RNText } from 'react-native'; 3 | import { TextPropTypes } from 'deprecated-react-native-prop-types'; 4 | import { connectAnimation } from '@shoutem/animation'; 5 | import { connectStyle } from '@shoutem/theme'; 6 | 7 | class Text extends PureComponent { 8 | render() { 9 | return ; 10 | } 11 | } 12 | 13 | Text.propTypes = { 14 | ...TextPropTypes, 15 | }; 16 | 17 | const AnimatedText = connectAnimation(Text); 18 | const StyledText = connectStyle('shoutem.ui.Text')(AnimatedText); 19 | const Heading = connectStyle('shoutem.ui.Heading')(AnimatedText); 20 | const Title = connectStyle('shoutem.ui.Title')(AnimatedText); 21 | const Subtitle = connectStyle('shoutem.ui.Subtitle')(AnimatedText); 22 | const Caption = connectStyle('shoutem.ui.Caption')(AnimatedText); 23 | 24 | export { Caption, Heading, Subtitle, StyledText as Text, Title }; 25 | -------------------------------------------------------------------------------- /components/Tile.js: -------------------------------------------------------------------------------- 1 | import { View } from 'react-native'; 2 | import { connectAnimation } from '@shoutem/animation'; 3 | import { connectStyle } from '@shoutem/theme'; 4 | 5 | const AnimatedTile = connectAnimation(View); 6 | const Tile = connectStyle('shoutem.ui.Tile')(AnimatedTile); 7 | 8 | export { Tile }; 9 | -------------------------------------------------------------------------------- /components/ToastMessage/components/ActionToast.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connectStyle } from '@shoutem/theme'; 3 | import BaseToast from './BaseToast'; 4 | 5 | function ActionToast({ style, isVisible, props }) { 6 | return ( 7 | 13 | ); 14 | } 15 | 16 | ActionToast.propTypes = BaseToast.propTypes; 17 | 18 | export default connectStyle('shoutem.ui.ActionToast')(ActionToast); 19 | -------------------------------------------------------------------------------- /components/ToastMessage/components/ErrorToast.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connectStyle } from '@shoutem/theme'; 3 | import BaseToast from './BaseToast'; 4 | 5 | function ErrorToast({ style, props, isVisible }) { 6 | return ( 7 | 13 | ); 14 | } 15 | 16 | ErrorToast.propTypes = BaseToast.propTypes; 17 | 18 | export default connectStyle('shoutem.ui.ErrorToast')(ErrorToast); 19 | -------------------------------------------------------------------------------- /components/ToastMessage/components/InfoToast.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connectStyle } from '@shoutem/theme'; 3 | import BaseToast from './BaseToast'; 4 | 5 | function InfoToast({ style, props, isVisible }) { 6 | return ( 7 | 8 | ); 9 | } 10 | 11 | InfoToast.propTypes = BaseToast.propTypes; 12 | 13 | export default connectStyle('shoutem.ui.InfoToast')(InfoToast); 14 | -------------------------------------------------------------------------------- /components/ToastMessage/components/SuccessToast.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connectStyle } from '@shoutem/theme'; 3 | import BaseToast from './BaseToast'; 4 | 5 | function SuccessToast({ style, props, isVisible }) { 6 | return ( 7 | 13 | ); 14 | } 15 | 16 | SuccessToast.propTypes = BaseToast.propTypes; 17 | 18 | export default connectStyle('shoutem.ui.SuccessToast')(SuccessToast); 19 | -------------------------------------------------------------------------------- /components/ToastMessage/components/ToastProgressBar.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useRef } from 'react'; 2 | import { Animated, View } from 'react-native'; 3 | import PropTypes from 'prop-types'; 4 | import { connectStyle } from '@shoutem/theme'; 5 | 6 | function ToastProgressBar({ 7 | duration, 8 | style, 9 | onProgressComplete, 10 | color, 11 | visible, 12 | }) { 13 | const progressValue = useRef(new Animated.Value(0)).current; 14 | 15 | const handleAnimationEnd = useCallback(() => { 16 | progressValue.setValue(0); 17 | if (onProgressComplete) { 18 | onProgressComplete(); 19 | } 20 | }, [onProgressComplete, progressValue]); 21 | 22 | useEffect(() => { 23 | if (!visible) { 24 | progressValue.setValue(0); 25 | return; 26 | } 27 | 28 | Animated.timing(progressValue, { 29 | toValue: 1, 30 | duration, 31 | useNativeDriver: false, 32 | }).start(handleAnimationEnd); 33 | }, [visible, duration, handleAnimationEnd]); 34 | 35 | return ( 36 | 37 | 49 | 50 | ); 51 | } 52 | 53 | ToastProgressBar.propTypes = { 54 | style: PropTypes.object.isRequired, 55 | visible: PropTypes.bool.isRequired, 56 | color: PropTypes.string, 57 | duration: PropTypes.number, 58 | onProgressComplete: PropTypes.func, 59 | }; 60 | 61 | ToastProgressBar.defaultProps = { 62 | color: '#00AADF', 63 | duration: 4000, 64 | onProgressComplete: undefined, 65 | }; 66 | 67 | export default connectStyle('shoutem.ui.ToastProgressBar')(ToastProgressBar); 68 | -------------------------------------------------------------------------------- /components/ToastMessage/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as ActionToast } from './ActionToast'; 2 | export { default as BaseToast } from './BaseToast'; 3 | export { default as ErrorToast } from './ErrorToast'; 4 | export { default as InfoToast } from './InfoToast'; 5 | export { default as SuccessToast } from './SuccessToast'; 6 | export { default as ToastProgressBar } from './ToastProgressBar'; 7 | -------------------------------------------------------------------------------- /components/ToastMessage/index.js: -------------------------------------------------------------------------------- 1 | export { BaseToast } from './components'; 2 | export { default as Toast } from './toastMessage'; 3 | -------------------------------------------------------------------------------- /components/Touchable.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { Platform } from 'react-native'; 3 | import PropTypes from 'prop-types'; 4 | import { connectStyle } from '@shoutem/theme'; 5 | import { TouchableNativeFeedback } from './TouchableNativeFeedback'; 6 | import { TouchableOpacity } from './TouchableOpacity'; 7 | import { View } from './View'; 8 | 9 | /** 10 | * A universal touchable component with a platform specific feedback. This 11 | * component displays a TouchableOpacity on iOS, and a TouchableNativeFeedback 12 | * on Android. 13 | */ 14 | class Touchable extends PureComponent { 15 | render() { 16 | const { children, style, styleName } = this.props; 17 | const { touchableNativeFeedback, touchableOpacity, ...otherStyle } = style; 18 | 19 | if (Platform.OS === 'android') { 20 | return ( 21 | 25 | 26 | {children} 27 | 28 | 29 | ); 30 | } 31 | 32 | return ( 33 | 40 | {children} 41 | 42 | ); 43 | } 44 | } 45 | 46 | Touchable.propTypes = { 47 | ...TouchableOpacity.propTypes, 48 | ...TouchableNativeFeedback.propTypes, 49 | style: PropTypes.object.isRequired, 50 | }; 51 | 52 | const StyledTouchable = connectStyle('shoutem.ui.Touchable', { 53 | touchableNativeFeedback: {}, 54 | touchableOpacity: {}, 55 | })(Touchable); 56 | 57 | export { StyledTouchable as Touchable }; 58 | -------------------------------------------------------------------------------- /components/TouchableNativeFeedback.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { TouchableNativeFeedback as RNTouchableNativeFeedback } from 'react-native'; 3 | import PropTypes from 'prop-types'; 4 | import { connectStyle } from '@shoutem/theme'; 5 | 6 | class TouchableNativeFeedback extends PureComponent { 7 | render() { 8 | const { children, style } = this.props; 9 | const { background, useForeground, ...otherStyle } = style; 10 | 11 | return ( 12 | 18 | {children} 19 | 20 | ); 21 | } 22 | } 23 | 24 | TouchableNativeFeedback.propTypes = { 25 | ...RNTouchableNativeFeedback.propTypes, 26 | style: PropTypes.shape({ 27 | background: PropTypes.object, 28 | useForeground: PropTypes.bool, 29 | }).isRequired, 30 | }; 31 | 32 | const StyledTouchableNativeFeedback = connectStyle( 33 | 'shoutem.ui.TouchableNativeFeedback', 34 | )(TouchableNativeFeedback); 35 | 36 | export { StyledTouchableNativeFeedback as TouchableNativeFeedback }; 37 | -------------------------------------------------------------------------------- /components/TouchableOpacity.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { TouchableOpacity as RNTouchableOpacity } from 'react-native'; 3 | import { connectStyle } from '@shoutem/theme'; 4 | 5 | class TouchableOpacity extends PureComponent { 6 | render() { 7 | const { children, style } = this.props; 8 | const { activeOpacity, ...otherStyle } = style; 9 | 10 | return ( 11 | 16 | {children} 17 | 18 | ); 19 | } 20 | } 21 | 22 | TouchableOpacity.propTypes = { 23 | ...RNTouchableOpacity.propTypes, 24 | }; 25 | 26 | const StyledTouchableOpacity = connectStyle('shoutem.ui.TouchableOpacity')( 27 | TouchableOpacity, 28 | ); 29 | 30 | export { StyledTouchableOpacity as TouchableOpacity }; 31 | -------------------------------------------------------------------------------- /components/Video/Video.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { View } from 'react-native'; 3 | import { WebView } from 'react-native-webview'; 4 | import PropTypes from 'prop-types'; 5 | import { connectAnimation } from '@shoutem/animation'; 6 | import { connectStyle } from '@shoutem/theme'; 7 | import VideoSourceReader from './VideoSourceReader'; 8 | 9 | function getSource(sourceReader, poster) { 10 | const url = sourceReader.getUrl(); 11 | 12 | if (sourceReader.isEmbeddableVideo()) { 13 | return { 14 | uri: url, 15 | }; 16 | } 17 | 18 | const HTML = ` 19 | 22 | `; 23 | 24 | return { 25 | html: HTML, 26 | }; 27 | } 28 | 29 | /** 30 | * Renders a Video based on the source type 31 | * if source is an url to a web player the 32 | * it is displayed in a WebView, if not 33 | * a Video HTML element is displayed in the 34 | * WebView. 35 | * 36 | * @returns {*} 37 | */ 38 | class Video extends PureComponent { 39 | constructor(props) { 40 | super(props); 41 | 42 | const { source, playerParams } = props; 43 | this.sourceReader = new VideoSourceReader(source.uri, playerParams); 44 | } 45 | 46 | render() { 47 | const { width, height = '100%', style, poster } = this.props; 48 | 49 | return ( 50 | 51 | 59 | 60 | ); 61 | } 62 | } 63 | 64 | Video.propTypes = { 65 | style: PropTypes.object.isRequired, 66 | height: PropTypes.number, 67 | // `playerParams` currently only works for Youtube 68 | playerParams: PropTypes.object, 69 | poster: PropTypes.string, 70 | source: PropTypes.shape({ 71 | uri: PropTypes.string, 72 | }), 73 | width: PropTypes.number, 74 | }; 75 | 76 | Video.defaultProps = { 77 | width: undefined, 78 | height: undefined, 79 | playerParams: { showinfo: 0 }, 80 | source: undefined, 81 | poster: undefined, 82 | }; 83 | 84 | const AnimatedVideo = connectAnimation(Video); 85 | const StyledVideo = connectStyle('shoutem.ui.Video')(AnimatedVideo); 86 | 87 | export { StyledVideo as Video }; 88 | -------------------------------------------------------------------------------- /components/Video/VideoSourceReader.js: -------------------------------------------------------------------------------- 1 | import { stringify } from 'qs'; 2 | 3 | function getYouTubeVideoId(url) { 4 | // eslint-disable-next-line no-useless-escape 5 | const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\??v?=?))([^#&\?]*).*/; 6 | const match = url.match(regExp); 7 | 8 | if (match && match[7].length === 11) { 9 | return match[7]; 10 | } 11 | 12 | return false; 13 | } 14 | 15 | function getVimeoVideoId(url) { 16 | // TODO(Vladimir) - find a shorter regex that covers all of our usecases, remove eslint-disable 17 | // The eslint line length rule is disabled so we can use our old battle-tested regex for vimeo 18 | // eslint-disable-next-line no-useless-escape 19 | const regExp = /https?:\/\/(?:[\w]+\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|album\/(\d+)\/video\/|video\/|)(\d+)(?:$|\/|\?)/; // eslint-disable-line max-len 20 | const match = url.match(regExp); 21 | 22 | if (match && match[3]) { 23 | return match[3]; 24 | } 25 | 26 | return false; 27 | } 28 | 29 | function getYouTubeEmbedUrl(id, playerParams) { 30 | const serializedParams = stringify(playerParams); 31 | return `https://www.youtube.com/embed/${id}?${serializedParams}`; 32 | } 33 | 34 | function getVimeoEmbedUrl(id) { 35 | return `https://player.vimeo.com/video/${id}?title=0&byline=0&portrait=0`; 36 | } 37 | 38 | /** 39 | * Reads the video source and provides the video 40 | * url in embedded form if necessary 41 | */ 42 | 43 | export default class VideoSourceReader { 44 | constructor(source, playerParams) { 45 | this.source = source; 46 | this.playerParams = playerParams; 47 | this.isYouTube = !!getYouTubeVideoId(source); 48 | this.isVimeo = !!getVimeoVideoId(source); 49 | } 50 | 51 | isEmbeddableVideo() { 52 | return this.isYouTube || this.isVimeo; 53 | } 54 | 55 | getUrl() { 56 | if (this.isYouTube) { 57 | return getYouTubeEmbedUrl( 58 | getYouTubeVideoId(this.source), 59 | this.playerParams, 60 | ); 61 | } 62 | if (this.isVimeo) { 63 | return getVimeoEmbedUrl(getVimeoVideoId(this.source)); 64 | } 65 | 66 | return this.source; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /components/Video/index.js: -------------------------------------------------------------------------------- 1 | export { Video } from './Video'; 2 | -------------------------------------------------------------------------------- /components/View.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { View as RNView } from 'react-native'; 3 | import { ViewPropTypes } from 'deprecated-react-native-prop-types'; 4 | import PropTypes from 'prop-types'; 5 | import { connectAnimation } from '@shoutem/animation'; 6 | import { connectStyle } from '@shoutem/theme'; 7 | import { LinearGradient } from './LinearGradient'; 8 | 9 | class View extends PureComponent { 10 | render() { 11 | const { children, style } = this.props; 12 | const { backgroundGradient, ...viewStyle } = style; 13 | 14 | let gradient = null; 15 | if (backgroundGradient) { 16 | gradient = ( 17 | 18 | ); 19 | } 20 | 21 | return ( 22 | 23 | {gradient} 24 | {children} 25 | 26 | ); 27 | } 28 | } 29 | 30 | View.propTypes = { 31 | ...ViewPropTypes, 32 | style: PropTypes.object.isRequired, 33 | }; 34 | 35 | const AnimatedView = connectAnimation(View); 36 | const StyledView = connectStyle('shoutem.ui.View')(AnimatedView); 37 | 38 | export { StyledView as View }; 39 | -------------------------------------------------------------------------------- /components/YearRangePicker/YearRangePickerButton.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { Animated } from 'react-native'; 3 | import autoBindReact from 'auto-bind/react'; 4 | import PropTypes from 'prop-types'; 5 | import { connectStyle } from '@shoutem/theme'; 6 | import { Icon } from '../Icon'; 7 | import { Text } from '../Text'; 8 | import { TouchableOpacity } from '../TouchableOpacity'; 9 | 10 | const AnimatedIcon = Animated.createAnimatedComponent(Icon); 11 | 12 | class YearRangePickerButton extends PureComponent { 13 | constructor(props) { 14 | super(props); 15 | 16 | autoBindReact(this); 17 | 18 | this.dropDownIconValue = new Animated.Value(0); 19 | } 20 | 21 | componentDidUpdate(prevProps) { 22 | const { collapsed } = this.props; 23 | const { collapsed: prevCollapsed } = prevProps; 24 | 25 | if (collapsed !== prevCollapsed) { 26 | const toValue = collapsed ? 1 : 0; 27 | 28 | Animated.timing(this.dropDownIconValue, { 29 | toValue, 30 | useNativeDriver: true, 31 | duration: 300, 32 | }).start(); 33 | } 34 | } 35 | 36 | render() { 37 | const { style, tooltip, onPress } = this.props; 38 | 39 | return ( 40 | 41 | {tooltip} 42 | 56 | 57 | ); 58 | } 59 | } 60 | 61 | YearRangePickerButton.propTypes = { 62 | style: PropTypes.object.isRequired, 63 | collapsed: PropTypes.bool, 64 | tooltip: PropTypes.string, 65 | onPress: PropTypes.func, 66 | }; 67 | 68 | YearRangePickerButton.defaultProps = { 69 | collapsed: undefined, 70 | tooltip: undefined, 71 | onPress: undefined, 72 | }; 73 | 74 | export default connectStyle('shoutem.ui.YearRangePickerButton')( 75 | YearRangePickerButton, 76 | ); 77 | -------------------------------------------------------------------------------- /components/YearRangePicker/index.js: -------------------------------------------------------------------------------- 1 | export { default as YearRangePicker } from './YearRangePicker'; 2 | -------------------------------------------------------------------------------- /const.js: -------------------------------------------------------------------------------- 1 | import { Platform, StatusBar } from 'react-native'; 2 | import { 3 | HOME_INDICATOR_PADDING, 4 | NAVIGATION_BAR_HEIGHT, 5 | NAVIGATION_HEADER_HEIGHT, 6 | NOTCH_AREA_HEIGHT, 7 | } from './helpers'; 8 | 9 | const STATUS_BAR_OFFSET = 10 | Platform.OS === 'android' ? -StatusBar.currentHeight : 0; 11 | 12 | // TODO: Deprecate and remove exports from here 13 | // Currently leaving for backwards compatibility 14 | export { 15 | HOME_INDICATOR_PADDING, 16 | NAVIGATION_BAR_HEIGHT, 17 | NAVIGATION_HEADER_HEIGHT, 18 | NOTCH_AREA_HEIGHT, 19 | STATUS_BAR_OFFSET, 20 | }; 21 | 22 | export const DEFAULT_PROGRESS_COLORS = [ 23 | '#FF0000', // Red 24 | '#FF4500', // Orange-Red 25 | '#FFA500', // Orange 26 | '#FFD700', // Yellow 27 | '#ADFF2F', // Yellow-Green 28 | '#90EE90', // Light Green 29 | ]; 30 | -------------------------------------------------------------------------------- /fonts/Rubik-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/fonts/Rubik-Black.ttf -------------------------------------------------------------------------------- /fonts/Rubik-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/fonts/Rubik-BlackItalic.ttf -------------------------------------------------------------------------------- /fonts/Rubik-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/fonts/Rubik-Bold.ttf -------------------------------------------------------------------------------- /fonts/Rubik-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/fonts/Rubik-BoldItalic.ttf -------------------------------------------------------------------------------- /fonts/Rubik-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/fonts/Rubik-Italic.ttf -------------------------------------------------------------------------------- /fonts/Rubik-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/fonts/Rubik-Light.ttf -------------------------------------------------------------------------------- /fonts/Rubik-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/fonts/Rubik-LightItalic.ttf -------------------------------------------------------------------------------- /fonts/Rubik-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/fonts/Rubik-Medium.ttf -------------------------------------------------------------------------------- /fonts/Rubik-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/fonts/Rubik-MediumItalic.ttf -------------------------------------------------------------------------------- /fonts/Rubik-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/fonts/Rubik-Regular.ttf -------------------------------------------------------------------------------- /fonts/rubicon-icon-font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/ui/f4bc3ecc3701c766c48729a2a4d1ccd74c2f45ad/fonts/rubicon-icon-font.ttf -------------------------------------------------------------------------------- /helpers/device-selector.js: -------------------------------------------------------------------------------- 1 | import { Platform, StatusBar } from 'react-native'; 2 | import DeviceInfo from 'react-native-device-info'; 3 | import { initialWindowMetrics } from 'react-native-safe-area-context'; 4 | 5 | export const NAVIGATION_HEADER_HEIGHT = 64; 6 | 7 | export const isNotchedAndroid = 8 | Platform.OS === 'android' && DeviceInfo.hasNotch(); 9 | 10 | export const NAVIGATION_BAR_HEIGHT = Platform.select({ 11 | ios: NAVIGATION_HEADER_HEIGHT + initialWindowMetrics?.insets?.top, 12 | android: isNotchedAndroid 13 | ? NAVIGATION_HEADER_HEIGHT + StatusBar.currentHeight 14 | : NAVIGATION_HEADER_HEIGHT, 15 | default: NAVIGATION_HEADER_HEIGHT, 16 | }); 17 | 18 | export const HOME_INDICATOR_PADDING = 19 | Platform.OS === 'ios' ? initialWindowMetrics?.insets?.bottom : 0; 20 | 21 | export const NOTCH_AREA_HEIGHT = Platform.select({ 22 | ios: initialWindowMetrics?.insets?.top, 23 | android: isNotchedAndroid ? StatusBar.currentHeight : 0, 24 | default: 0, 25 | }); 26 | 27 | export const Device = { 28 | isNotchedAndroid, 29 | HOME_INDICATOR_PADDING, 30 | NOTCH_AREA_HEIGHT, 31 | NAVIGATION_HEADER_HEIGHT, 32 | }; 33 | -------------------------------------------------------------------------------- /helpers/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | Device, 3 | HOME_INDICATOR_PADDING, 4 | isNotchedAndroid, 5 | NAVIGATION_BAR_HEIGHT, 6 | NAVIGATION_HEADER_HEIGHT, 7 | NOTCH_AREA_HEIGHT, 8 | } from './device-selector'; 9 | export { calculateKeyboardOffset, default as Keyboard } from './keyboard'; 10 | -------------------------------------------------------------------------------- /helpers/keyboard.js: -------------------------------------------------------------------------------- 1 | import { Platform, StatusBar } from 'react-native'; 2 | import { isNotchedAndroid, NAVIGATION_BAR_HEIGHT } from './device-selector'; 3 | 4 | export function calculateKeyboardOffset(extraOffset = 0) { 5 | const resolvedOffset = NAVIGATION_BAR_HEIGHT + extraOffset; 6 | 7 | if (Platform.OS === 'ios' || isNotchedAndroid) { 8 | return resolvedOffset; 9 | } 10 | 11 | if (Platform.OS === 'android' && !isNotchedAndroid) { 12 | return StatusBar.currentHeight + resolvedOffset; 13 | } 14 | 15 | return resolvedOffset; 16 | } 17 | 18 | // TODO: Deprecate and remove Keyboard.calculateKeyboardOffset 19 | // Replace with direct function call. It's cleaner for any practical use of this 20 | // because we will usually import Keyboard from react-native alongside this, so 21 | // we're forced to rename imports. 22 | export default { calculateKeyboardOffset }; 23 | -------------------------------------------------------------------------------- /hooks/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | useColorAndPercentageInterpolation, 3 | useColorInterpolation, 4 | } from './useColorAndPercentageInterpolation'; 5 | -------------------------------------------------------------------------------- /html/README.md: -------------------------------------------------------------------------------- 1 | # Html 2 | 3 | ## Html component 4 | Transforms the raw HTML to the React Native components tree. 5 | Accepts these props: 6 | 7 | - `body` A raw HTML. 8 | - `renderElement(element, style, defaultRenderElement)` A function to override default `renderElement`. If this function returns `undefined` the default `renderElement` will be used to render element. 9 | - `style` A Html style. 10 | 11 | Register an element with the static `Html.registerElement(elementTag, Component)` method. 12 | Registered elements will be handled with the default `renderElement`. 13 | 14 | ### Components 15 | Components in the `html/components` folder are base implementation of common HTML elements that contain specific logic. 16 | Wrap those components with your own element implementation, and reduce the element attributes to the component props. 17 | 18 | For example, there are different ways to implement the HTML image gallery but most the most cases you can use the same React Native implementation. 19 | 20 | ## Element components 21 | Elements are React Native components used to render a parsed element. 22 | The Element will by default receives these props: 23 | 24 | - `element` An object describing element, contains `tag`, `attributes`, `childElements`, `parent` props. 25 | - `style` The [Shoutem theme](https://github.com/shoutem/theme) style passed in the Html instance style. 26 | - `renderElement` A function used to render an element with registered element component. 27 | 28 | -------------------------------------------------------------------------------- /html/components/AttachmentRenderer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import PropTypes from 'prop-types'; 4 | import { resolveDimensions } from '../services/Dimensions'; 5 | import Image from './Image'; 6 | 7 | const AttachmentRenderer = ({ id, type, style, attachments }) => { 8 | if (!attachments) { 9 | return null; 10 | } 11 | 12 | if (type === 'image') { 13 | const image = _.find(attachments, { id }); 14 | 15 | if (image && image.src) { 16 | const source = { uri: image.src }; 17 | const imageSize = { width: image.width, height: image.height }; 18 | const { height, width } = resolveDimensions(imageSize, style); 19 | const resolvedStyle = { alignSelf: 'center', height, width }; 20 | 21 | return ; 22 | } 23 | } 24 | 25 | return null; 26 | }; 27 | 28 | AttachmentRenderer.propTypes = { 29 | attachments: PropTypes.oneOf([PropTypes.array, PropTypes.object]), 30 | id: PropTypes.any, 31 | style: PropTypes.any, 32 | type: PropTypes.any, 33 | }; 34 | 35 | AttachmentRenderer.defaultProps = { 36 | id: undefined, 37 | type: undefined, 38 | attachments: undefined, 39 | style: undefined, 40 | }; 41 | 42 | export default AttachmentRenderer; 43 | -------------------------------------------------------------------------------- /html/components/Gallery.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import autoBindReact from 'auto-bind/react'; 3 | import PropTypes from 'prop-types'; 4 | import { InlineGallery } from '../../components/InlineGallery'; 5 | 6 | /** 7 | * Use to render a HTML gallery component. 8 | * Style interface correspond to InlineGallery from @shoutem/ui. 9 | */ 10 | export default class Gallery extends PureComponent { 11 | constructor(props) { 12 | super(props); 13 | 14 | autoBindReact(this); 15 | 16 | this.state = { 17 | selectedIndex: 0, 18 | }; 19 | } 20 | 21 | onIndexSelected(selectedIndex) { 22 | this.setState({ selectedIndex }); 23 | } 24 | 25 | handlePhotoPress() { 26 | const { data, handlePhotoPress } = this.props; 27 | const { selectedIndex } = this.state; 28 | 29 | if (!handlePhotoPress) { 30 | // eslint-disable-next-line no-console 31 | console.warn('There is no "handlePhotoPress" handler for Gallery photo.'); 32 | return; 33 | } 34 | 35 | handlePhotoPress(data, selectedIndex); 36 | } 37 | 38 | render() { 39 | const { selectedIndex } = this.state; 40 | 41 | return ( 42 | 48 | ); 49 | } 50 | } 51 | 52 | Gallery.propTypes = { 53 | ...InlineGallery.propTypes, 54 | handlePhotoPress: PropTypes.func, 55 | }; 56 | 57 | Gallery.defaultProps = { 58 | handlePhotoPress: undefined, 59 | }; 60 | -------------------------------------------------------------------------------- /html/components/VideoRenderer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | useComputeMaxWidthForTag, 4 | useContentWidth, 5 | } from 'react-native-render-html'; 6 | import WebView from 'react-native-webview'; 7 | import { findOne } from 'domutils'; 8 | import PropTypes from 'prop-types'; 9 | 10 | function findSource(tnode) { 11 | if (tnode.attributes.src) { 12 | return tnode.attributes.src; 13 | } 14 | 15 | const sourceElms = findOne( 16 | elm => elm.tagName === 'source', 17 | tnode.domNode.children, 18 | ); 19 | 20 | return sourceElms ? sourceElms.attribs.src : ''; 21 | } 22 | 23 | /** 24 | * 25 | * Generate video html with only selected attributes and parameters. 26 | * @returns HTML video element 27 | */ 28 | function generateVideoHtml(tnode) { 29 | const url = findSource(tnode); 30 | 31 | const containsTimeParameter = url.match(/(#|\?)t=[^&$]*/g, ''); 32 | // If video has no time parameter, we want to add time parameter at 0.1, just to be able to 33 | // display the image, first frame of the video. Video tags that don't have autoplay attribute 34 | // are not showing first frame without this solution. 35 | // Otherwise, if user has defined time parameter, keep it as is. 36 | const resolvedUrl = `${url}${containsTimeParameter ? '' : '#t=0.1'}`; 37 | 38 | return `