Repository: SKempin/Lyrics-King-React-Native Branch: master Commit: 15918ffb1585 Files: 21 Total size: 41.5 KB Directory structure: gitextract_37vbbbyq/ ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── App.js ├── App.test.js ├── LICENSE ├── README.md ├── _design_assets/ │ └── adobeXD/ │ └── Lyrics King App.xd ├── app.json ├── components/ │ ├── Credits.js │ ├── SocialButton.js │ └── Suggestions.js ├── config/ │ ├── colours.js │ └── router.js ├── lib/ │ └── constants.js ├── package.json ├── screens/ │ ├── AboutScreen.js │ ├── DetailsScreen.js │ └── SearchScreen.js └── utils/ └── shareHelper.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": ["module:metro-react-native-babel-preset"], "plugins": [ [ "@babel/plugin-proposal-decorators", { "legacy": true } ] ] } ================================================ FILE: .eslintrc.json ================================================ { "extends": "airbnb", "env": { "browser": true, "es6": true, "jest/globals": true }, "settings": { "import/resolver": { "node": { "extensions": [".js", ".android.js", ".ios.js"] } } }, "plugins": ["jest"], "parser": "babel-eslint", "rules": { "no-use-before-define": 0, "import/prefer-default-export": 0, "react/jsx-filename-extension": [ "error", { "extensions": [".js", ".jsx"] } ], "react/prefer-stateless-function": 0, "react/jsx-indent-props": ["error", 2], "react/jsx-indent": ["error", 2], "consistent-return": 0, "max-len": 0, "react/forbid-prop-types": 0, "no-nested-ternary": 0, "no-console": 0, "no-mixed-operators": 0, "react/no-array-index-key": 0, "camelcase": 0, "no-underscore-dangle": 0, "react/sort-comp": 0, "no-return-assign": 0, "comma-dangle": ["error", "never"] } } ================================================ FILE: .gitignore ================================================ # See https://help.github.com/ignore-files/ for more about ignoring files. */*.DS_Store *.DS_Store # expo .expo/ # dependencies /node_modules # misc .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* package-lock.json yarn.lock .vscode ================================================ FILE: .travis.yml ================================================ # # Travis CI config # https://docs.travis-ci.com/user/customizing-the-build/ # env: - COVERALLS_ENV=production branches: only: - master language: node_js node_js: - "11.2.0" cache: directories: - "node_modules" script: - npm run lint ================================================ FILE: App.js ================================================ import React from 'react'; import Sentry from 'sentry-expo'; import { createAppContainer } from 'react-navigation'; import { RootStack } from './config/router'; // Remove this once Sentry is correctly setup. Sentry.enableInExpoDevelopment = false; Sentry.config( 'https://705f92b0edb44599b814955f3219c1cd@sentry.io/1367138', ).install(); // App Containers const AppContainer = createAppContainer(RootStack); export default class App extends React.Component { render() { return ; } } ================================================ FILE: App.test.js ================================================ import React from 'react'; import renderer from 'react-test-renderer'; import App from './App'; it('renders without crashing', () => { const rendered = renderer.create().toJSON(); expect(rendered).toBeTruthy(); }); ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Stephen Kempin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Lyrics King ![](https://img.shields.io/github/license/SKempin/Lyrics-King-React-Native.svg?style=flat-square) ![](https://img.shields.io/github/stars/SKempin/Lyrics-King-React-Native.svg?style=flat-square) ![](https://img.shields.io/github/forks/SKempin/Lyrics-King-React-Native.svg?style=flat-square) [![Build Status](https://travis-ci.org/SKempin/Lyrics-King-React-Native.svg?branch=master)](https://travis-ci.org/SKempin/Lyrics-King-React-Native) [![Mentioned in Awesome Expo](https://awesome.re/mentioned-badge.svg)](https://github.com/expo/awesome-expo) A [React Native](https://facebook.github.io/react-native/) native app utilising [Expo](https://expo.io/), [React Navigation](https://reactnavigation.org) and fetching data from multiple API's ([Deezer](https://developers.deezer.com/) and [Lyrics.OVH](https://www.lyrics.ovh)). UI built with [Adobe XD](https://www.adobe.com/uk/products/xd.html).

Built as a personal training project for [React Native](https://facebook.github.io/react-native/). Designed in [Adobe XD](https://www.adobe.com/uk/products/xd.html). Design and development by [Stephen Kempin](https://www.stephenkempin.co.uk). This project was bootstrapped with [Create React Native App](https://github.com/react-community/create-react-native-app). ### [Expo Demo Link](https://expo.io/@skempin/lyrics-king) Lyrics King - React Native Expo app ## Contents - [App Preview](#app-preview) - [Video Preview](#video-preview) - [Search Screen](#search-screen) - [Details Screen](#details-screen) - [About Screen](#about-screen) - [Navigation (Drawer)](#navigation-drawer) - [Expo Project Page](#expo-project-page) - [Adobe XD files](#adobe-xd-files) - [App Features](#app-features) - [Screens](#screens) - [Components](#components) - [Config](#config) - [Lib](#lib) - [Utils](#utils) - [Getting Started](#getting-started) - [What's Included](#whats-included) - [API's Used](#apis-used) - [Contributing](#contributing) - [Author](#author) - [Google Play Store](#google-play-store) - [Donate](#donate) - [License](#license) ## App Preview ### Video Preview ### Search Screen Lyrics King - Search screenLyrics King - Suggestions on search screen ### Details Screen Lyrics King - Details screen, Ariana GrandeLyrics King - Details screen, Above and BeyondLyrics King - Details screen, Dua Lipa ### About Screen Lyrics King - About screen ### Navigation (Drawer) Lyrics King - Navigation drawer ## [Expo Project Page](https://expo.io/@skempin/lyrics-king) This project has been built using [Expo](https://expo.io/). Please install `npm install expo-cli --global` to run this project locally. Scan the below QR code to open the project on Android: ![](https://github.com/SKempin/Lyrics-King-React-Native/blob/master/_github/qr.png)
## Adobe XD files Design files for the UI can be found in `_design_assets/adobeXD` in the project root. UI design implemented with [flexbox](https://docs.expo.io/versions/latest/react-native/flexbox). ## App Features ### Screens `src/screens/` - `SearchScreen.js` - Search the [Deezer API](https://developers.deezer.com/) by song title (_class component_) - `DetailsScreen.js` - Selected song details (including [Lyrics.ovh](https://www.lyrics.ovh/) API call) (_class component_) - `AboutScreen.js` - About details (_functional component_) ### Components `src/components/` - `Credits.js` - Development credentials template (_functional component_) - `SocialButton.js` - Button template for sharing links/ the app (_functional component_) - `Suggestions.js` - Song suggestions (_functional component_) ### Config `src/config/` - `router.js` - App navigation routing (including drawer nav render method) - `colours.js` - Colour constants ### Lib `src/lib/` - `constants.js` - Expo manifest [constants](https://docs.expo.io/versions/latest/sdk/constants#__next) and functions ### Utils `src/utils/` - `shareHelper.js` - Native device [share method](https://docs.expo.io/versions/latest/react-native/share) ## Getting Started 1. Install the latest Node 2. Install [Expo](https://expo.io/) - `npm install expo-cli --global` 3. `cd` into this project directory 4. `npm install` or `yarn install` 5. Run `expo start` ## What's Included | Name | Description | | :----------------------------------------------------------------: | ------------------------------------------------------------------------------------------------------------------------------------------------ | | [Expo (incl. React Native)](https://expo.io/) | Expo is a free and open source toolchain built around React Native to help you build native iOS and Android projects using JavaScript and React. | | [React Navigation](https://reactnavigation.org/) | Routing and navigation for your React Native apps. | | [Format Duration](https://github.com/hypermodules/format-duration) | Convert a number in milliseconds to a standard duration string. | | [RN-Placeholder](https://github.com/mfrachet/rn-placeholder) | Display some placeholder stuff before rendering your text or media content in React Native. | ## API's Used - [Deezer](https://developers.deezer.com/) - [Lyrics.OVH](https://api.lyrics.ovh) ## Contributing Due to time constraints there are several features that I haven’t been able to develop yet. If you would like to develop your React Native skills and contribute any of the features below this would be hugely beneficial! :tada: - [x] [Debouncing or throttling](https://www.peterbe.com/plog/how-to-throttle-and-debounce-an-autocomplete-input-in-react) on search functionality. - [x] [PropTypes](https://reactjs.org/docs/typechecking-with-proptypes.html) on components. - [ ] Adding clear search button functionality on Android. This functionality [already exists on iOS](https://facebook.github.io/react-native/docs/textinput#clearbuttonmode). - [ ] [Animations](https://docs.expo.io/versions/latest/react-native/animations) would be a nice touch! Fading in the details screen background image would be priority. - [ ] Any general performance improvements. Other contributions and suggestions are always very welcome! [Contact me](https://www.stephenkempin.co.uk) if you wish to discuss anything. ## Author [Stephen Kempin](https://www.stephenkempin.co.uk) [Lyrics King Project Github](https://github.com/SKempin/Lyrics-King-React-Native) ## Google Play Store View my commercial apps on the [SK-UK Google Play Store](https://play.google.com/store/apps/developer?id=SK+-+UK) SK-UK Google Play Store ## Donate If you like this project and wish to say to say thanks - I'm always open to a coffee! :coffee: Buy Me A Coffee ## License [MIT](https://github.com/SKempin/Lyrics-King-React-Native/blob/master/LICENSE) You are welcome to use this however you wish within the MIT license, but please retain [my credentials](https://www.stephenkempin.co.uk/) and links back to [this repo](https://github.com/SKempin/Lyrics-King-React-Native). ================================================ FILE: app.json ================================================ { "expo": { "name": "Lyrics King", "description": "Lyrics King is a lyrics search app, fetching data from Deezer an Lyrics.ovh. Designed with Adobe XD and built with Expo, by Stephen Kempin. https://github.com/SKempin/Lyrics-King-React-Native", "icon": "./assets/images/icon.png", "slug": "lyrics-king", "sdkVersion": "32.0.0", "platforms": ["ios", "android"], "githubUrl":"https://github.com/SKempin/Lyrics-King-React-Native", "orientation": "portrait", "privacy": "public", "primaryColor": "#07CCBA", "splash": { "backgroundColor": "#191919 ", "image": "./assets/images/splash.png", "resizeMode": "cover" }, "extra": { "appName": "Lyrics King", "developerName": "SK-UK", "googleAnalytics": "UA-131961084", "social": { "expoApp": "https://expo.io/@skempin/lyrics-king", "github": "https://github.com/SKempin/Lyrics-King-React-Native", "googlePlayStore": "https://play.google.com/store/apps/developer?id=SK+-+UK", "iTunesStore": "https://www.stephenkempin.co.uk", "portfolio": "https://www.stephenkempin.co.uk" } } } } ================================================ FILE: components/Credits.js ================================================ import React from 'react'; import { Text, TouchableOpacity, StyleSheet, Linking, Image } from 'react-native'; import Proptypes from 'prop-types'; import * as Expo from 'expo'; import { Analytics, Event } from 'expo-analytics'; import SK from '../assets/images/SK.png'; // Config import colours from '../config/colours'; // Constants const { portfolio } = Expo.Constants.manifest.extra.social; const ID = Expo.Constants.manifest.extra.googleAnalytics; // GA tracking const analytics = new Analytics(ID); const Credits = props => ( Linking.openURL(portfolio).then(() => { Expo.Amplitude.logEvent(`BUTTON: Credits - ${props.screen} Screen`); analytics.event( new Event('Button', 'Tap', `Credits - ${props.screen} Screen`) ); }) } > Design and development {'\n'} {' '} by Stephen Kempin ); Credits.propTypes = { screen: Proptypes.string.isRequired }; export default Credits; // Styles const styles = StyleSheet.create({ creditsContainer: { flexDirection: 'row', alignItems: 'center' }, creditsText: { fontSize: 12, color: colours.secondaryGrey, textAlign: 'left', paddingLeft: 20 }, creditsImage: { width: 30, height: 30, opacity: 0.2, alignSelf: 'flex-start' } }); ================================================ FILE: components/SocialButton.js ================================================ import React from 'react'; import { Text, TouchableOpacity, StyleSheet, Linking, Image } from 'react-native'; import * as Expo from 'expo'; import PropTypes from 'prop-types'; /* eslint-disable import/no-extraneous-dependencies */ import { Entypo, EvilIcons } from '@expo/vector-icons'; /* eslint-enable import/no-extraneous-dependencies */ import { Analytics, Event } from 'expo-analytics'; import SK from '../assets/images/SK.png'; // Config import colours from '../config/colours'; // Helper functions import handleShare from '../utils/shareHelper'; // Constants const { ...extra } = Expo.Constants.manifest.extra; const ID = Expo.Constants.manifest.extra.googleAnalytics; // GA tracking const analytics = new Analytics(ID); const SocialButton = ({ label, url, screen, icon }) => ( (label !== 'Share' ? Linking.openURL(url).then(() => { Expo.Amplitude.logEvent(`BUTTON: ${label}`); analytics.event(new Event('Button', 'Tap', `${label}`)); }) : handleShare( `Check out ${extra.appName} on Expo today!`, `${extra.social.expoApp}`, `${extra.appName}`, `${screen}` )) } > {icon ? ( ) : ( )} {label} ); SocialButton.propTypes = { icon: PropTypes.string, label: PropTypes.string.isRequired, url: PropTypes.string, screen: PropTypes.string.isRequired }; SocialButton.defaultProps = { icon: '', url: '' }; export default SocialButton; // Styles const styles = StyleSheet.create({ button: { padding: 10, backgroundColor: colours.tertiaryBlack, flex: 1, flexDirection: 'row', flexWrap: 'nowrap', alignItems: 'center', justifyContent: 'flex-start', marginBottom: 10 }, icon_left: { marginRight: 15, color: colours.primaryGrey }, icon_sk: { marginRight: 15, opacity: 0.5, width: 22, height: 22 }, label: { flex: 1, color: colours.primaryGrey }, icon_right: { color: colours.primaryGrey, justifyContent: 'flex-end', opacity: 0.4 } }); ================================================ FILE: components/Suggestions.js ================================================ import React from 'react'; import { Image, FlatList, Text, TouchableOpacity, Keyboard, StyleSheet, View } from 'react-native'; import PropTypes from 'prop-types'; /* eslint-disable import/no-extraneous-dependencies */ import { EvilIcons } from '@expo/vector-icons'; /* eslint-enable import/no-extraneous-dependencies */ // Config import colours from '../config/colours'; const Suggestions = ({ results, navigation }) => ( ( navigation.navigate('Details', { ...item })} > {item.title_short} {item.artist.name} {item.album.title} )} keyExtractor={(item, index) => index.toString()} /> ); Suggestions.propTypes = { results: PropTypes.array.isRequired, navigation: PropTypes.object.isRequired }; export default Suggestions; // Styles const styles = StyleSheet.create({ suggestionItem: { flex: 1, flexDirection: 'row', flexWrap: 'nowrap', backgroundColor: colours.tertiaryBlack, elevation: 1, justifyContent: 'flex-start', alignItems: 'center', paddingTop: 12, paddingBottom: 12, paddingLeft: 18, paddingRight: 12, marginLeft: 14, marginRight: 14, marginTop: 0, marginBottom: 10 }, image: { width: 66, height: 66, borderRadius: 66 / 2, alignSelf: 'center', borderColor: colours.primaryWhite, borderWidth: 2, marginRight: 17, flex: 0 }, detailsContainer: { width: 145, marginRight: 20 }, songTitle: { color: 'white', paddingBottom: 2 }, artistDetails: { color: colours.primaryGrey, paddingBottom: 2 } }); ================================================ FILE: config/colours.js ================================================ // Colours const colours = { primaryBlack: '#101010', secondaryBlack: '#1D1D1D', tertiaryBlack: '#141414', highlightBlack: '#0B0B0B', primaryWhite: '#fff', primaryTeal: '#07CCBA', primaryGrey: '#AAAAAA', secondaryGrey: '#3E3E3E' }; export default colours; ================================================ FILE: config/router.js ================================================ import React from 'react'; import { createDrawerNavigator, createStackNavigator, DrawerItems } from 'react-navigation'; import * as Expo from 'expo'; /* eslint-disable import/no-extraneous-dependencies */ import { Ionicons } from '@expo/vector-icons'; /* eslint-enable import/no-extraneous-dependencies */ import { StyleSheet, Image, ScrollView, SafeAreaView, View } from 'react-native'; import Logo from '../assets/images/lk-logo.png'; // Components import SocialButton from '../components/SocialButton'; // Helper functions import handleShare from '../utils/shareHelper'; // Config import colours from './colours'; // Constants import { socialLinks } from '../lib/constants'; // Screens import SearchScreen from '../screens/SearchScreen'; import DetailsScreen from '../screens/DetailsScreen'; import AboutScreen from '../screens/AboutScreen'; const { ...extra } = Expo.Constants.manifest.extra; // Main stack export const MainStack = createStackNavigator({ Search: { screen: SearchScreen, navigationOptions: ({ navigation }) => ({ title: null, headerTransparent: true, headerLeft: ( navigation.openDrawer()} /> ) }) }, Details: { screen: DetailsScreen, navigationOptions: ({ navigation }) => ({ headerStyle: { borderBottomWidth: 0, backgroundColor: 'rgba(0,0,0,0.2)', elevation: 0 }, headerTransparent: true, headerTintColor: 'rgba(255,255,255,0.7)', headerRight: ( handleShare( `Check out lyrics for ${navigation.state.params.title} by ${ navigation.state.params.artist.name } on ${extra.appName}!`, `${extra.social.expoApp}`, `${extra.appName}`, 'Details' ) } /> ) }) } }); // About stack export const AboutStack = createStackNavigator({ About: { screen: AboutScreen, navigationOptions: ({ navigation }) => ({ title: null, headerTransparent: true, headerLeft: ( navigation.openDrawer()} /> ) }) } }); // ===================================================== // Side drawer export const RootStack = createDrawerNavigator( { Search: { screen: MainStack }, About: { screen: AboutStack } }, { contentComponent: props => ( {socialLinks.map((socialLink, index) => ( ))} ) } ); // Styles const styles = StyleSheet.create({ container: { flex: 1, marginTop: 50 }, logo: { width: 150, height: 150, alignSelf: 'center', marginBottom: 30 }, itemStyle: { borderLeftWidth: 3, borderLeftColor: colours.primaryTeal }, socialLinksContainer: { marginLeft: 20, marginRight: 20, marginTop: 45, fontSize: 4, paddingTop: 30, borderTopColor: colours.secondaryGrey, borderTopWidth: 1 } }); ================================================ FILE: lib/constants.js ================================================ import * as Expo from 'expo'; const { ...social } = Expo.Constants.manifest.extra.social; // Expo constants export const { expoVersion } = Expo.Constants; // export const iOSBuild = Constants.platform.ios.buildNumber; export const { manifest } = Expo.Constants; // functions export const getCurrentYear = new Date().getFullYear(); // Social Links export const socialLinks = [ { icon: 'google-play', label: 'SK-UK Play Store', url: social.googlePlayStore }, { icon: 'app-store', label: 'SK-UK iTunes Store', url: social.iTunesStore }, { icon: 'github', label: 'Github Project', url: social.github }, { icon: '', label: 'SK Portfolio', url: social.portfolio } ]; ================================================ FILE: package.json ================================================ { "name": "lyrics-king", "version": "1.0.3", "private": true, "author": "Stephen Kempin (https://stephenkempin.co.uk/)", "license": "MIT", "keywords": [ "expo", "react-native", "react", "es6", "android", "ios" ], "devDependencies": { "babel-eslint": "^10.0.1", "eslint": "^5.3.0", "eslint-config-airbnb": "^17.1.0", "eslint-plugin-import": "^2.16.0", "eslint-plugin-jest": "^22.2.2", "eslint-plugin-jsx-a11y": "^6.2.0", "eslint-plugin-react": "^7.12.4", "jest-expo": "^32.0.0", "react-test-renderer": "16.3.1" }, "main": "./node_modules/expo/AppEntry.js", "scripts": { "start": "react-native-scripts start", "eject": "react-native-scripts eject", "android": "react-native-scripts android", "ios": "react-native-scripts ios", "lint": "./node_modules/.bin/eslint .", "test": "jest" }, "jest": { "preset": "jest-expo" }, "dependencies": { "expo": "^32.0.0", "expo-analytics": "^1.0.7", "format-duration": "^1.3.1", "prop-types": "^15.6.2", "react": "16.14.0", "react-native": "https://github.com/expo/react-native/archive/sdk-32.0.0.tar.gz", "react-navigation": "^3.0.9", "rn-placeholder": "^1.3.0", "sentry-expo": "^1.11.0", "throttle-debounce": "^2.1.0" } } ================================================ FILE: screens/AboutScreen.js ================================================ import React from 'react'; import { Text, View, StyleSheet, ScrollView, Image, SafeAreaView, StatusBar } from 'react-native'; import * as Expo from 'expo'; // Config import { Analytics, ScreenHit } from 'expo-analytics'; import colours from '../config/colours'; // Components import SocialButton from '../components/SocialButton'; import { getCurrentYear, socialLinks, manifest } from '../lib/constants'; import SK_LOGO from '../assets/images/lk-logo.png'; // Constants const { ...meta } = Expo.Constants.manifest; // GA tracking const ID = Expo.Constants.manifest.extra.googleAnalytics; const analytics = new Analytics(ID); export default class AboutScreen extends React.Component { componentDidMount() { Expo.Amplitude.logEvent('SCREEN: About'); analytics.hit(new ScreenHit('SCREEN: About')); } render() { return ( Social {socialLinks.map((socialLink, index) => ( ))} Share Donate App Info {`${meta.extra.appName}: ${manifest.version}`} © {' '} {getCurrentYear} {' '} {meta.extra.developerName} . Design & development by {' '} {meta.extra.developerName} . Expo SDK: {' '} {meta.sdkVersion} Released under MIT licence. ); } } // Styles const styles = StyleSheet.create({ safeView: { flex: 1, backgroundColor: colours.primaryBlack }, container: { flex: 1, backgroundColor: colours.primaryBlacks, flexDirection: 'column', paddingLeft: 30, paddingRight: 30, paddingTop: 40 }, headingText: { color: colours.primaryTeal, fontSize: 20, marginBottom: 20, marginTop: 30, fontWeight: '300' }, detailsContainer: { paddingBottom: 20 }, detailsContainerText: { color: colours.primaryGrey, fontSize: 12, paddingBottom: 6 }, heading: { paddingTop: 50, paddingBottom: 15, backgroundColor: colours.primaryBlack }, logo: { width: 76, height: 76, marginBottom: 20 } }); ================================================ FILE: screens/DetailsScreen.js ================================================ import React from 'react'; import { StyleSheet, Text, Image, View, ScrollView, ImageBackground } from 'react-native'; import * as Expo from 'expo'; import PropTypes from 'prop-types'; import format from 'format-duration'; import Placeholder from 'rn-placeholder'; import { Analytics, ScreenHit } from 'expo-analytics'; // Config import colours from '../config/colours'; // Components import Credits from '../components/Credits'; // GA tracking const ID = Expo.Constants.manifest.extra.googleAnalytics; const analytics = new Analytics(ID); export default class DetailsScreen extends React.Component { static get propTypes() { return { navigation: PropTypes.object.isRequired }; } constructor(props) { super(props); this.state = { lyrics: null, /* err: null, */ isReady: null }; } async componentDidMount() { const { navigation: { state: { params: { title, artist } } } } = this.props; Expo.Amplitude.logEvent(`SCREEN - Details: ${title} by ${artist.name}`); analytics.hit( new ScreenHit(`SCREEN - Details: ${title} by ${artist.name}`) ); const lyricsQuery = `${artist.name}/${title}`; this.getLyrics(lyricsQuery); } // fix issue #5 - setting state on unmounted component componentWillUnmount() { this.isCancelled = true; } getLyrics = async (lyricsQuery) => { try { const res = await fetch(`https://api.lyrics.ovh/v1/${lyricsQuery}`); const response = await res.json(); if (!this.isCancelled) this.setState({ lyrics: response.lyrics, isReady: true }); } catch (e) { console.log(e); // this.setState({ err: e.message }); } }; displayLyrics() { const { lyrics, isReady } = this.state; if (!lyrics) { return ( Sorry, no lyrics can be found for this song. ); } return ( {lyrics} ); } render() { const { navigation: { state: { params: { title, artist, album, duration } } } } = this.props; return ( {artist.name} {title} Album {album.title} Duration {format(duration * 1000)} {this.displayLyrics()} ); } } // Styles const styles = StyleSheet.create({ container: { backgroundColor: colours.primaryBlack, flex: 1 }, backgroundImage: { flex: 1, minHeight: 360, flexDirection: 'row' }, gradient: { backgroundColor: 'transparent', position: 'absolute', top: 0, bottom: 0, left: 0, right: 0 }, artistHeading: { color: colours.primaryWhite, fontSize: 35, lineHeight: 35, fontWeight: '300', paddingBottom: 7, shadowOpacity: 0.6, shadowRadius: 3, shadowOffset: { height: 0, width: 0 } }, songHeading: { color: colours.primaryWhite, fontSize: 45, lineHeight: 45, fontWeight: 'bold', paddingBottom: 0, shadowOpacity: 0.6, shadowRadius: 3, shadowOffset: { height: 0, width: 0 } }, albumImage: { width: 130, height: 130, borderRadius: 130 / 2, borderWidth: 3, borderColor: colours.primaryWhite, marginRight: 25 }, detailsHeading: { color: colours.primaryGrey, marginBottom: 3 }, details: { color: colours.primaryWhite, fontWeight: 'bold', marginBottom: 15, fontSize: 16 }, lyrics: { color: colours.primaryWhite, lineHeight: 22, paddingBottom: 20 }, creditsContainer: { flex: 1, alignSelf: 'center', paddingTop: 40, paddingBottom: 30 } }); ================================================ FILE: screens/SearchScreen.js ================================================ import React from 'react'; import { StyleSheet, TextInput, View, Image, Keyboard, SafeAreaView, StatusBar, TouchableWithoutFeedback } from 'react-native'; import * as Expo from 'expo'; import PropTypes from 'prop-types'; /* eslint-disable import/no-extraneous-dependencies */ import { EvilIcons } from '@expo/vector-icons'; /* eslint-enable import/no-extraneous-dependencies */ import { Analytics, ScreenHit } from 'expo-analytics'; // search throlle and debounce import { throttle, debounce } from 'throttle-debounce'; import LK_LOGO from '../assets/images/lk-logo.png'; import SK from '../assets/images/SK.png'; // Config import colours from '../config/colours'; // Components import Suggestions from '../components/Suggestions'; import Credits from '../components/Credits'; // Cache images function cacheImages(images) { return images.map((image) => { if (typeof image === 'string') { return Image.prefetch(image); } return Expo.Asset.fromModule(image).downloadAsync(); }); } // GA tracking const ID = Expo.Constants.manifest.extra.googleAnalytics; const analytics = new Analytics(ID); export default class SearchScreen extends React.Component { static get propTypes() { return { navigation: PropTypes.object.isRequired }; } constructor(props) { super(props); this.state = { results: [], text: null, showLogo: true }; this.throttleSearch = throttle(400, this.getInfo); this.debounceSearch = debounce(700, this.getInfo); this.cache = {}; // caching autocomplete results } componentDidMount() { Expo.Amplitude.initialize('6460727d017e832e2083e13916c7c9e5'); Expo.Amplitude.logEvent('SCREEN: Search'); analytics.hit(new ScreenHit('SCREEN: Search')); } componentDidUpdate(prevProps, prevState) { const { text } = this.state; if (text !== prevState.text) { if (text.length >= 1) { if (text.length < 5 || text.endsWith(' ')) this.throttleSearch(text); else this.debounceSearch(text); } else { this.submitAndClear(); } } } // Load logos _loadAssetsAsync = async () => { const imageAssets = cacheImages([LK_LOGO, SK]); await Promise.all([...imageAssets]); }; getInfo = () => { const { text } = this.state; const url = `https://api.deezer.com/search?q=track:"${text}"&limit=20&order=RANKING?strict=on`; const cached = this.cache[url]; if (cached) { this.setState({ results: cached, showLogo: false }); return; } fetch(url) .then(response => response.json()) .then((data) => { this.cache[url] = data.data; this.setState({ results: data.data, showLogo: false }); }); }; submitAndClear = () => { this.setState({ text: '', showLogo: true }); Keyboard.dismiss(); }; render() { const { isReady, showLogo, text, results } = this.state; const { navigation } = this.props; if (!isReady) { return ( this.setState({ isReady: true })} onError={console.warn} /> ); } return ( {showLogo && } this.setState({ text: changedText }) } value={text} placeholder="Search song" placeholderTextColor="#fff" clearButtonMode="always" /> {results.length > 0 && text.length > 0 && ( )} {showLogo && } ); } } // Styles const styles = StyleSheet.create({ safeView: { flex: 1, backgroundColor: colours.primaryBlack }, container: { flex: 1, backgroundColor: colours.primaryBlack, alignItems: 'center', justifyContent: 'center', paddingTop: 40, paddingBottom: 30 }, logo: { width: 160, height: 160, marginBottom: 60, marginTop: 40 }, searchContainer: { flexDirection: 'row', flexWrap: 'nowrap', width: 280, paddingTop: 18, paddingBottom: 18, paddingRight: 20, paddingLeft: 10, marginBottom: 20, backgroundColor: colours.highlightBlack, alignItems: 'center', justifyContent: 'center' }, TextInput: { flex: 1, fontSize: 16, textAlign: 'center', alignItems: 'center', flexWrap: 'nowrap', color: colours.primaryWhite }, Suggestions: { flex: 1, alignItems: 'center', color: colours.primaryWhite }, creditsContainer: { flexDirection: 'row', width: 170 }, creditsText: { fontSize: 12, color: colours.secondaryGrey, textAlign: 'left', paddingLeft: 20 }, creditsImage: { width: 30, height: 30, opacity: 0.2, alignSelf: 'flex-start' } }); ================================================ FILE: utils/shareHelper.js ================================================ import { Share } from 'react-native'; import * as Expo from 'expo'; import { Analytics, Event } from 'expo-analytics'; // GA tracking const ID = Expo.Constants.manifest.extra.googleAnalytics; const analytics = new Analytics(ID); const handleShare = (message, url, title, screen) => { Share.share( { message, url, title }, { // Android only: dialogTitle: `Share ${title}`, // iOS only: excludedActivityTypes: ['com.apple.UIKit.activity.PostToTwitter'] } ).then(() => { Expo.Amplitude.logEvent(`BUTTON: Share - ${screen} Screen`); analytics.event(new Event('Button', 'Tap', `Share - ${screen} Screen`)); }); }; export default handleShare;