├── .expo-shared └── assets.json ├── .gitignore ├── App.js ├── README.md ├── app.json ├── assets ├── custom-theme.json ├── favicon.png ├── icon.png ├── icon2.png ├── logo.png ├── logo2.png ├── placeholder.jpg ├── splash.png ├── splash2.png └── twitter_PNG9.png ├── babel.config.js ├── components ├── CategoryBar.js ├── DetailNews.js ├── Slider.js ├── TabBarIcon.js ├── card │ ├── NewsCard.js │ └── SliderCard.js ├── loading │ ├── DetailNewsLoading.js │ ├── LoadingNewsCard.js │ ├── LoadingSearch.js │ ├── LoadingSlider.js │ ├── LoadingSliderCard.js │ └── Page.js ├── page │ ├── Page.js │ ├── PageSaved.js │ └── PageSearch.js └── topnav │ ├── SavedTopNav.js │ ├── SearchTopNav.js │ └── TopNav.js ├── navigation └── AppNavigator.js ├── package-lock.json ├── package.json ├── preview.gif ├── privacy-policy.md ├── screens ├── Detail.js ├── Home.js ├── Media.js ├── Saved.js ├── Search.js └── rubrik │ ├── Rubrik.js │ └── Search.js ├── ss.png ├── term-of-service.md └── thumbnail.jpg /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p8 6 | *.p12 7 | *.key 8 | *.mobileprovision 9 | *.orig.* 10 | web-build/ 11 | 12 | # macOS 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { AppLoading } from 'expo'; 3 | import { Asset } from 'expo-asset'; 4 | import * as Font from 'expo-font'; 5 | import { StatusBar, StyleSheet } from 'react-native'; 6 | import { ApplicationProvider, IconRegistry } from '@ui-kitten/components'; 7 | import { mapping, light as lightTheme } from '@eva-design/eva'; 8 | import { EvaIconsPack } from '@ui-kitten/eva-icons'; 9 | import AppNavigator from './navigation/AppNavigator'; 10 | import { SafeAreaView } from 'react-native-safe-area-context'; 11 | import { default as theme } from './assets/custom-theme.json'; 12 | import { ApolloProvider, Query } from 'react-apollo'; 13 | import ApolloClient from 'apollo-boost'; 14 | import moment from 'moment'; 15 | import 'moment/locale/id'; 16 | 17 | const client = new ApolloClient({ 18 | uri: 'https://obscure-ridge-07773.herokuapp.com/graphql', 19 | }); 20 | export default function App(props) { 21 | const [isLoadingComplete, setLoadingComplete] = useState(false); 22 | 23 | if (!isLoadingComplete && !props.skipLoadingScreen) { 24 | return ( 25 | handleFinishLoading(setLoadingComplete)} 29 | /> 30 | ); 31 | } else { 32 | return ( 33 | 34 | 38 | 39 | 40 | 41 | 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | } 53 | } 54 | 55 | async function loadResourcesAsync() { 56 | await Promise.all([ 57 | Asset.loadAsync([ 58 | require('./assets/icon.png'), 59 | require('./assets/splash.png'), 60 | require('./assets/logo.png'), 61 | require('./assets/placeholder.jpg'), 62 | ]), 63 | ]); 64 | } 65 | 66 | function handleLoadingError(error) { 67 | // In this case, you might want to report the error to your error reporting 68 | // service, for example Sentry 69 | console.warn(error); 70 | } 71 | 72 | function handleFinishLoading(setLoadingComplete) { 73 | setLoadingComplete(true); 74 | } 75 | const styles = StyleSheet.create({ 76 | container: { 77 | flex: 1, 78 | }, 79 | }); 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redesign & Rebuild "Sehat Negeriku" Kemenkes news portal app 2 | 3 | ![thumbnail.jpg](thumbnail.jpg) 4 | 5 | This is unofficial Redesign & Rebuild project by me. 6 | 7 | ## Using : 8 | 9 | - React Native 10 | - Expo 11 | - Graphql 12 | - React navigation 13 | - UI Kitten 14 | 15 | ## To do : 16 | 17 | - [x] Delete some unused files 18 | - [x] Refactoring some components 19 | - [x] Implementing infinite scroll 20 | - [x] In detail page (single page) article no need to request data 21 | - [x] Implementing shared element for navigation transitions 22 | - [x] Upload to playstore 🥳 23 | 24 | ## Update : 25 | 26 | - Shared element transitions 27 | 28 | ![preview.gif](preview.gif) 29 | 30 | - Better reading experience 31 | 32 | ![ss.png](ss.png) 33 | 34 | Full read case study : [https://twitter.com/kikiding/status/1302211106958573568](https://twitter.com/kikiding/status/1302211106958573568) 35 | 36 | Try it on expo : [https://expo.io/@kidingki/sehatnegeriku](https://expo.io/@kidingki/sehatnegeriku) 37 | 38 | Download on Playstore : [https://play.google.com/store/apps/details?id=com.kikiding.sehatnegerikurebuild](https://play.google.com/store/apps/details?id=com.kikiding.sehatnegerikurebuild) 39 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Sehat Negeriku Rebuild", 4 | "slug": "sehatnegeriku", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "splash": { 9 | "image": "./assets/splash.png", 10 | "resizeMode": "contain", 11 | "backgroundColor": "#ffffff" 12 | }, 13 | "updates": { 14 | "fallbackToCacheTimeout": 0 15 | }, 16 | "assetBundlePatterns": [ 17 | "**/*" 18 | ], 19 | "android": { 20 | "package": "com.kikiding.sehatnegerikurebuild", 21 | "versionCode": 1 22 | }, 23 | "ios": { 24 | "supportsTablet": true 25 | }, 26 | "web": { 27 | "favicon": "./assets/favicon.png" 28 | }, 29 | "description": "" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /assets/custom-theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "color-primary-100": "#CFFBE9", 3 | "color-primary-200": "#A0F7DC", 4 | "color-primary-300": "#6EE8CD", 5 | "color-primary-400": "#47D1BF", 6 | "color-primary-500": "#16B3AC", 7 | "color-primary-600": "#109399", 8 | "color-primary-700": "#0B7180", 9 | "color-primary-800": "#075267", 10 | "color-primary-900": "#043C55", 11 | "color-primary-transparent-100": "rgba(22, 179, 172, 0.08)", 12 | "color-primary-transparent-200": "rgba(22, 179, 172, 0.16)", 13 | "color-primary-transparent-300": "rgba(22, 179, 172, 0.24)", 14 | "color-primary-transparent-400": "rgba(22, 179, 172, 0.32)", 15 | "color-primary-transparent-500": "rgba(22, 179, 172, 0.4)", 16 | "color-primary-transparent-600": "rgba(22, 179, 172, 0.48)", 17 | "color-success-100": "#EEFAD1", 18 | "color-success-200": "#DAF5A6", 19 | "color-success-300": "#B7E174", 20 | "color-success-400": "#90C44E", 21 | "color-success-500": "#609E1F", 22 | "color-success-600": "#4B8716", 23 | "color-success-700": "#38710F", 24 | "color-success-800": "#275B09", 25 | "color-success-900": "#1C4B05", 26 | "color-success-transparent-100": "rgba(96, 158, 31, 0.08)", 27 | "color-success-transparent-200": "rgba(96, 158, 31, 0.16)", 28 | "color-success-transparent-300": "rgba(96, 158, 31, 0.24)", 29 | "color-success-transparent-400": "rgba(96, 158, 31, 0.32)", 30 | "color-success-transparent-500": "rgba(96, 158, 31, 0.4)", 31 | "color-success-transparent-600": "rgba(96, 158, 31, 0.48)", 32 | "color-info-100": "#CEEBFE", 33 | "color-info-200": "#9DD4FE", 34 | "color-info-300": "#6CB8FE", 35 | "color-info-400": "#489EFD", 36 | "color-info-500": "#0C74FC", 37 | "color-info-600": "#0859D8", 38 | "color-info-700": "#0642B5", 39 | "color-info-800": "#032E92", 40 | "color-info-900": "#022078", 41 | "color-info-transparent-100": "rgba(12, 116, 252, 0.08)", 42 | "color-info-transparent-200": "rgba(12, 116, 252, 0.16)", 43 | "color-info-transparent-300": "rgba(12, 116, 252, 0.24)", 44 | "color-info-transparent-400": "rgba(12, 116, 252, 0.32)", 45 | "color-info-transparent-500": "rgba(12, 116, 252, 0.4)", 46 | "color-info-transparent-600": "rgba(12, 116, 252, 0.48)", 47 | "color-warning-100": "#FFF9CC", 48 | "color-warning-200": "#FFF399", 49 | "color-warning-300": "#FFEA67", 50 | "color-warning-400": "#FFE241", 51 | "color-warning-500": "#FFD402", 52 | "color-warning-600": "#DBB201", 53 | "color-warning-700": "#B79201", 54 | "color-warning-800": "#937300", 55 | "color-warning-900": "#7A5D00", 56 | "color-warning-transparent-100": "rgba(255, 212, 2, 0.08)", 57 | "color-warning-transparent-200": "rgba(255, 212, 2, 0.16)", 58 | "color-warning-transparent-300": "rgba(255, 212, 2, 0.24)", 59 | "color-warning-transparent-400": "rgba(255, 212, 2, 0.32)", 60 | "color-warning-transparent-500": "rgba(255, 212, 2, 0.4)", 61 | "color-warning-transparent-600": "rgba(255, 212, 2, 0.48)", 62 | "color-danger-100": "#FFE7D7", 63 | "color-danger-200": "#FFCAAF", 64 | "color-danger-300": "#FFA687", 65 | "color-danger-400": "#FF8469", 66 | "color-danger-500": "#FF4B38", 67 | "color-danger-600": "#DB2B28", 68 | "color-danger-700": "#B71C26", 69 | "color-danger-800": "#931125", 70 | "color-danger-900": "#7A0A24", 71 | "color-danger-transparent-100": "rgba(255, 75, 56, 0.08)", 72 | "color-danger-transparent-200": "rgba(255, 75, 56, 0.16)", 73 | "color-danger-transparent-300": "rgba(255, 75, 56, 0.24)", 74 | "color-danger-transparent-400": "rgba(255, 75, 56, 0.32)", 75 | "color-danger-transparent-500": "rgba(255, 75, 56, 0.4)", 76 | "color-danger-transparent-600": "rgba(255, 75, 56, 0.48)" 77 | } -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingki/sehatnegeriku/b5e1115be4c5c17ecbb51f52529d6e27c68ca7b9/assets/favicon.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingki/sehatnegeriku/b5e1115be4c5c17ecbb51f52529d6e27c68ca7b9/assets/icon.png -------------------------------------------------------------------------------- /assets/icon2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingki/sehatnegeriku/b5e1115be4c5c17ecbb51f52529d6e27c68ca7b9/assets/icon2.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingki/sehatnegeriku/b5e1115be4c5c17ecbb51f52529d6e27c68ca7b9/assets/logo.png -------------------------------------------------------------------------------- /assets/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingki/sehatnegeriku/b5e1115be4c5c17ecbb51f52529d6e27c68ca7b9/assets/logo2.png -------------------------------------------------------------------------------- /assets/placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingki/sehatnegeriku/b5e1115be4c5c17ecbb51f52529d6e27c68ca7b9/assets/placeholder.jpg -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingki/sehatnegeriku/b5e1115be4c5c17ecbb51f52529d6e27c68ca7b9/assets/splash.png -------------------------------------------------------------------------------- /assets/splash2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingki/sehatnegeriku/b5e1115be4c5c17ecbb51f52529d6e27c68ca7b9/assets/splash2.png -------------------------------------------------------------------------------- /assets/twitter_PNG9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingki/sehatnegeriku/b5e1115be4c5c17ecbb51f52529d6e27c68ca7b9/assets/twitter_PNG9.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /components/CategoryBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, View, StyleSheet, Dimensions } from 'react-native'; 3 | 4 | const { width } = Dimensions.get('window'); 5 | const category = 0.034 * width; 6 | export default function TopNav() { 7 | return ( 8 | 18 | 25 | Artikel Terbaru 26 | 27 | 34 | Rilis Sehat 35 | 36 | 43 | Blog Sehat 44 | 45 | 52 | Infografis 53 | 54 | 61 | Daerah 62 | 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /components/DetailNews.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Text, 4 | View, 5 | StyleSheet, 6 | ScrollView, 7 | Image, 8 | Dimensions, 9 | } from 'react-native'; 10 | import HTML from 'react-native-render-html'; 11 | import moment from 'moment'; 12 | import { SharedElement } from 'react-navigation-shared-element'; 13 | const { width } = Dimensions.get('window'); 14 | 15 | function Detail(props) { 16 | const { data } = props; 17 | function check() { 18 | if (data.thumbnail_images) { 19 | if (data.thumbnail_images.full) { 20 | if (data.thumbnail_images.full.url) { 21 | return true; 22 | } 23 | } 24 | } 25 | } 26 | 27 | return ( 28 | 29 | {check() ? ( 30 | 31 | 38 | 39 | ) : ( 40 | 41 | )} 42 | 43 | 53 | {data.title} 54 | 55 | {data.author.name} |{' '} 56 | {moment(data.date).format('dddd, Do MMMM YYYY HH mm')} 57 | 58 | 59 | 68 | 77 | 78 | 79 | ); 80 | } 81 | Detail.sharedElements = (props) => { 82 | const { data } = props; 83 | return [ 84 | { 85 | id: `data.${data.id}`, 86 | }, 87 | ]; 88 | }; 89 | export default Detail; 90 | -------------------------------------------------------------------------------- /components/Slider.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Dimensions } from 'react-native'; 3 | import Swiper from 'react-native-swiper'; 4 | import ShimmerPlaceHolder from 'react-native-shimmer-placeholder'; 5 | import SliderCard from './card/SliderCard'; 6 | const { width } = Dimensions.get('window'); 7 | export default function Slider(props) { 8 | const isFetched = props.isFetched; 9 | const { data } = props; 10 | return ( 11 | 17 | 18 | 35 | } 36 | > 37 | {data !== undefined && 38 | data.map((item, index) => { 39 | if (index <= 2) { 40 | return ( 41 | 46 | ); 47 | } 48 | })} 49 | 50 | 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /components/TabBarIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Icon } from '@ui-kitten/components'; 3 | export default function TabBarIcon(props) { 4 | return ( 5 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /components/card/NewsCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Text, 4 | View, 5 | Image, 6 | Dimensions, 7 | TouchableWithoutFeedback, 8 | } from 'react-native'; 9 | import ShimmerPlaceHolder from 'react-native-shimmer-placeholder'; 10 | import moment from 'moment'; 11 | import { SharedElement } from 'react-navigation-shared-element'; 12 | 13 | export default function NewsCard(props) { 14 | const { isFetched, data } = props; 15 | function check() { 16 | if (data.thumbnail_images) { 17 | if (data.thumbnail_images.full) { 18 | if (data.thumbnail_images.full.url) { 19 | return true; 20 | } 21 | } 22 | } 23 | } 24 | 25 | return ( 26 | { 28 | props.navigation.push('Detail', { 29 | id: data.id, 30 | category: data.categories[0].title, 31 | data: data, 32 | }); 33 | }} 34 | > 35 | 41 | 48 | {check() && ( 49 | 50 | 57 | 58 | )} 59 | 60 | 68 | 73 | 80 | {data.title} 81 | 82 | 83 | 88 | 95 | {data.categories[0].title} |{' '} 96 | {moment(data.date).format('dddd, Do MMMM YYYY')} 97 | 98 | 99 | 100 | 101 | 102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /components/card/SliderCard.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | Text, 4 | View, 5 | StyleSheet, 6 | ScrollView, 7 | Image, 8 | Dimensions, 9 | TouchableWithoutFeedback, 10 | } from 'react-native'; 11 | import { LinearGradient } from 'expo-linear-gradient'; 12 | import moment from 'moment'; 13 | 14 | import { SharedElement } from 'react-navigation-shared-element'; 15 | const { width, height } = Dimensions.get('window'); 16 | export default function SliderCard(props) { 17 | const { data } = props; 18 | 19 | function check() { 20 | if (data.thumbnail_images) { 21 | if (data.thumbnail_images.full) { 22 | if (data.thumbnail_images.full.url) { 23 | return true; 24 | } 25 | } 26 | } 27 | } 28 | return ( 29 | { 31 | props.navigation.navigate('Detail', { 32 | id: data.id, 33 | category: data.categories[0].title, 34 | data: data, 35 | }); 36 | }} 37 | > 38 | 45 | 46 | 55 | 56 | 57 | 68 | 76 | 84 | {data.title} 85 | 86 | 87 | 93 | {moment(data.date).format('dddd, Do MMMM YYYY HH:mm')} 94 | 95 | 96 | 109 | {data.categories[0].title} 110 | 111 | 112 | 113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /components/loading/DetailNewsLoading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Text, 4 | View, 5 | StyleSheet, 6 | ScrollView, 7 | Image, 8 | Dimensions, 9 | } from 'react-native'; 10 | import ShimmerPlaceHolder from 'react-native-shimmer-placeholder'; 11 | const { width } = Dimensions.get('window'); 12 | 13 | export default function Detail() { 14 | return ( 15 | 16 | 21 | 29 | 30 | 39 | 43 | Loading 44 | 45 | 49 | 50 | John Doe | Loading 51 | 52 | 53 | 54 | 55 | 64 | 65 | Loading 66 | 67 | 72 | Loading 73 | 74 | 79 | Loading 80 | 81 | 86 | Loading 87 | 88 | 93 | Loading 94 | 95 | 96 | 97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /components/loading/LoadingNewsCard.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | Text, 4 | View, 5 | StyleSheet, 6 | ScrollView, 7 | Image, 8 | Dimensions, 9 | } from 'react-native'; 10 | import ShimmerPlaceHolder from 'react-native-shimmer-placeholder'; 11 | import moment from 'moment'; 12 | const { width, height } = Dimensions.get('window'); 13 | export default function NewsCard(props) { 14 | const { isFetched, data } = props; 15 | 16 | return ( 17 | 23 | 30 | Loading 31 | 32 | 40 | 45 | 51 | Loading 52 | 53 | 54 | 59 | 66 | Loading | Loading 67 | 68 | 69 | 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /components/loading/LoadingSearch.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, StyleSheet, Dimensions } from 'react-native'; 3 | import NewsCard from './LoadingNewsCard'; 4 | const { width } = Dimensions.get('window'); 5 | export default function Page(props) { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | const styles = StyleSheet.create({ 21 | container: { 22 | flex: 1, 23 | backgroundColor: '#f5f5f5', 24 | }, 25 | newsCardContainer: { 26 | backgroundColor: 'white', 27 | borderColor: '#e5e5e5', 28 | borderTopWidth: 1, 29 | flex: 1, 30 | paddingHorizontal: 15, 31 | paddingVertical: 7.5, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /components/loading/LoadingSlider.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | Text, 4 | View, 5 | StyleSheet, 6 | ScrollView, 7 | Image, 8 | Dimensions, 9 | } from 'react-native'; 10 | import { Icon } from '@ui-kitten/components'; 11 | import { Ionicons } from '@expo/vector-icons'; 12 | import { LinearGradient } from 'expo-linear-gradient'; 13 | import Swiper from 'react-native-swiper'; 14 | import ShimmerPlaceHolder from 'react-native-shimmer-placeholder'; 15 | import SliderCard from './LoadingSliderCard'; 16 | const { width, height } = Dimensions.get('window'); 17 | export default function Slider(props) { 18 | const isFetched = props.isFetched; 19 | return ( 20 | 26 | 27 | 44 | } 45 | > 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /components/loading/LoadingSliderCard.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | Text, 4 | View, 5 | StyleSheet, 6 | ScrollView, 7 | Image, 8 | Dimensions, 9 | } from 'react-native'; 10 | 11 | import { LinearGradient } from 'expo-linear-gradient'; 12 | 13 | export default function SliderCard(props) { 14 | const { data } = props; 15 | function check() { 16 | if (data.thumbnail_images) { 17 | if (data.thumbnail_images.full) { 18 | if (data.thumbnail_images.full.url) { 19 | return true; 20 | } 21 | } 22 | } 23 | } 24 | return ( 25 | 32 | 40 | 41 | 52 | 60 | 67 | Loading 68 | 69 | 70 | 76 | Loading 77 | 78 | 79 | 92 | {' '} 93 | Loading 94 | 95 | 96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /components/loading/Page.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, StyleSheet, Dimensions } from 'react-native'; 3 | import Slider from './LoadingSlider'; 4 | import NewsCard from './LoadingNewsCard'; 5 | const { width } = Dimensions.get('window'); 6 | export default function PageLoading(props) { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | const styles = StyleSheet.create({ 22 | container: { 23 | flex: 1, 24 | backgroundColor: '#f5f5f5', 25 | }, 26 | newsCardContainer: { 27 | backgroundColor: 'white', 28 | borderColor: '#e5e5e5', 29 | borderTopWidth: 1, 30 | flex: 1, 31 | paddingHorizontal: 15, 32 | paddingVertical: 7.5, 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /components/page/Page.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { View, StyleSheet, Dimensions } from 'react-native'; 3 | import Slider from '../Slider'; 4 | import NewsCard from '../card/NewsCard'; 5 | const { width } = Dimensions.get('window'); 6 | export default function Page(props) { 7 | const { news, isFetched } = props; 8 | const [berita, setBerita] = useState( 9 | props.rubrik == 'latest' ? news.posts : news.category 10 | ); 11 | 12 | return ( 13 | 14 | 19 | 20 | 21 | {berita !== undefined && 22 | berita.map((item, index) => { 23 | if (index > 2) 24 | return ( 25 | 31 | ); 32 | })} 33 | 34 | 35 | ); 36 | } 37 | 38 | const styles = StyleSheet.create({ 39 | container: { 40 | flex: 1, 41 | backgroundColor: '#f5f5f5', 42 | }, 43 | newsCardContainer: { 44 | backgroundColor: 'white', 45 | borderColor: '#e5e5e5', 46 | borderTopWidth: 1, 47 | flex: 1, 48 | paddingHorizontal: 15, 49 | paddingVertical: 7.5, 50 | }, 51 | }); 52 | -------------------------------------------------------------------------------- /components/page/PageSaved.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Text, View, StyleSheet, Dimensions } from 'react-native'; 3 | import NewsCard from '../card/NewsCard'; 4 | const { width, height } = Dimensions.get('window'); 5 | export default function Page(props) { 6 | const [] = useState(props.news); 7 | return ( 8 | 9 | {props.news !== null ? ( 10 | 11 | {props.news.map((result, i, store) => { 12 | // get at each store's key/value so you can work with it 13 | let item = store[i][1]; 14 | const pars = JSON.parse(item); 15 | console.log(pars); 16 | return ( 17 | 23 | ); 24 | })} 25 | 26 | ) : ( 27 | 33 | 39 | Tidak ada berita yang disimpan 40 | 41 | 42 | )} 43 | 44 | ); 45 | } 46 | 47 | const styles = StyleSheet.create({ 48 | container: { 49 | flex: 1, 50 | backgroundColor: '#f5f5f5', 51 | }, 52 | newsCardContainer: { 53 | backgroundColor: 'white', 54 | 55 | flex: 1, 56 | paddingHorizontal: 15, 57 | paddingVertical: 7.5, 58 | }, 59 | }); 60 | -------------------------------------------------------------------------------- /components/page/PageSearch.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { View, StyleSheet, Dimensions } from 'react-native'; 3 | import NewsCard from '../card/NewsCard'; 4 | const { width } = Dimensions.get('window'); 5 | export default function Page(props) { 6 | const { news, isFetched } = props; 7 | const [berita] = useState(news.search); 8 | 9 | return ( 10 | 11 | 12 | {berita !== undefined && 13 | berita.map((item) => { 14 | return ( 15 | 21 | ); 22 | })} 23 | 24 | 25 | ); 26 | } 27 | 28 | const styles = StyleSheet.create({ 29 | container: { 30 | flex: 1, 31 | backgroundColor: '#f5f5f5', 32 | }, 33 | newsCardContainer: { 34 | backgroundColor: 'white', 35 | borderColor: '#e5e5e5', 36 | borderTopWidth: 1, 37 | flex: 1, 38 | paddingHorizontal: 15, 39 | paddingVertical: 7.5, 40 | }, 41 | }); 42 | -------------------------------------------------------------------------------- /components/topnav/SavedTopNav.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { View, StyleSheet, Image, Dimensions, TextInput } from 'react-native'; 3 | 4 | const { width } = Dimensions.get('window'); 5 | export default function TopNav(props) { 6 | const [] = useState(false); 7 | const [search, setSearch] = useState(''); 8 | return ( 9 | 14 | 25 | 26 | 30 | 31 | 40 | { 51 | setSearch(text); 52 | }} 53 | onSubmitEditing={() => { 54 | props.navigation.navigate('Search', { 55 | search: search, 56 | }); 57 | }} 58 | /> 59 | 60 | 61 | 62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /components/topnav/SearchTopNav.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | View, 4 | StyleSheet, 5 | Dimensions, 6 | TouchableOpacity, 7 | TextInput, 8 | } from 'react-native'; 9 | 10 | import { Icon } from '@ui-kitten/components'; 11 | const { width } = Dimensions.get('window'); 12 | export default function TopNav(props) { 13 | const [] = useState(false); 14 | const [search, setSearch] = useState(props.search); 15 | return ( 16 | 21 | 31 | { 33 | props.navigation.goBack(); 34 | }} 35 | style={{ flex: 1, alignItems: 'flex-start' }} 36 | > 37 | 38 | 39 | 48 | { 59 | setSearch(text); 60 | }} 61 | onSubmitEditing={() => { 62 | props.navigation.navigate('Search', { 63 | search: search, 64 | }); 65 | }} 66 | /> 67 | 68 | 69 | 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /components/topnav/TopNav.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | Text, 4 | View, 5 | StyleSheet, 6 | ScrollView, 7 | Image, 8 | Dimensions, 9 | TouchableOpacity, 10 | TextInput, 11 | } from 'react-native'; 12 | import { Ionicons } from '@expo/vector-icons'; 13 | 14 | const { width, height } = Dimensions.get('window'); 15 | const category = 0.034 * width; 16 | export default function TopNav(props) { 17 | const [searchToggle, setSearchToggle] = useState(false); 18 | const [search, setSearch] = useState(''); 19 | return ( 20 | 25 | 35 | 36 | 40 | 41 | 50 | {searchToggle ? ( 51 | { 63 | setSearch(text); 64 | }} 65 | onSubmitEditing={() => { 66 | props.navigation.navigate('Search', { 67 | search: search, 68 | }); 69 | }} 70 | /> 71 | ) : ( 72 | 18 ? 18 : 0.0437 * width, 77 | }} 78 | > 79 | Sehat Negeriku 80 | 81 | )} 82 | 83 | { 90 | setSearchToggle(!searchToggle); 91 | }} 92 | > 93 | 94 | 95 | 96 | 97 | ); 98 | } 99 | 100 | const styles = StyleSheet.create({ 101 | container: { 102 | flex: 1, 103 | backgroundColor: '#f5f5f5', 104 | }, 105 | }); 106 | -------------------------------------------------------------------------------- /navigation/AppNavigator.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Text, View } from 'react-native'; 3 | import { NavigationContainer } from '@react-navigation/native'; 4 | import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; 5 | import { createSharedElementStackNavigator } from 'react-navigation-shared-element'; 6 | 7 | import TabBarIcon from '../components/TabBarIcon'; 8 | import Home from '../screens/Home'; 9 | import Detail from '../screens/Detail'; 10 | import Media from '../screens/Media'; 11 | import Search from '../screens/Search'; 12 | import Saved from '../screens/Saved'; 13 | 14 | const Tab = createBottomTabNavigator(); 15 | 16 | const Stack = createSharedElementStackNavigator(); 17 | 18 | const Tabs = () => { 19 | return ( 20 | 25 | ( 30 | 38 | Home 39 | 40 | ), 41 | tabBarIcon: ({ focused }) => ( 42 | 43 | ), 44 | }} 45 | sharedElementsConfig={(route, otherRoute, showing) => { 46 | const { data } = route.params; 47 | return [`data.${data.id}`]; 48 | }} 49 | /> 50 | ( 55 | 63 | Media 64 | 65 | ), 66 | tabBarIcon: ({ focused }) => ( 67 | 68 | ), 69 | }} 70 | /> 71 | ( 76 | 84 | Saved 85 | 86 | ), 87 | tabBarIcon: ({ focused }) => ( 88 | 89 | ), 90 | }} 91 | /> 92 | 93 | ); 94 | }; 95 | 96 | const Stacks = () => { 97 | return ( 98 | 104 | 105 | 106 | ({ 110 | gestureEnabled: false, 111 | transitionSpec: { 112 | open: { animation: 'timing', config: { duration: 500 } }, 113 | close: { animation: 'timing', config: { duration: 250 } }, 114 | }, 115 | cardStyleInterpolator: ({ current: { progress } }) => { 116 | return { 117 | cardStyle: { 118 | opacity: progress, 119 | }, 120 | }; 121 | }, 122 | })} 123 | /> 124 | 125 | ); 126 | }; 127 | export default () => { 128 | return ( 129 | 130 | 131 | 132 | ); 133 | }; 134 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "web": "expo start --web", 8 | "eject": "expo eject" 9 | }, 10 | "dependencies": { 11 | "@apollo/client": "^3.1.4", 12 | "@eva-design/eva": "^2.0.0", 13 | "@expo/vector-icons": "^10.0.0", 14 | "@react-native-community/async-storage": "~1.11.0", 15 | "@react-native-community/masked-view": "0.1.10", 16 | "@react-navigation/bottom-tabs": "^5.2.1", 17 | "@react-navigation/native": "^5.0.9", 18 | "@react-navigation/stack": "^5.1.1", 19 | "@ui-kitten/components": "^5.0.0", 20 | "@ui-kitten/eva-icons": "^5.0.0", 21 | "apollo-boost": "^0.4.9", 22 | "expo": "~38.0.8", 23 | "expo-linear-gradient": "~8.2.1", 24 | "expo-status-bar": "^1.0.2", 25 | "graphql-tag": "^2.11.0", 26 | "lodash": "^4.17.20", 27 | "moment": "^2.27.0", 28 | "react": "~16.11.0", 29 | "react-apollo": "^3.1.5", 30 | "react-dom": "~16.11.0", 31 | "react-native": "https://github.com/expo/react-native/archive/sdk-38.0.2.tar.gz", 32 | "react-native-gesture-handler": "~1.6.0", 33 | "react-native-image-header-scroll-view": "^0.10.3", 34 | "react-native-reanimated": "~1.9.0", 35 | "react-native-render-html": "^4.2.3", 36 | "react-native-safe-area-context": "~3.0.7", 37 | "react-native-screens": "~2.9.0", 38 | "react-native-shared-element": "^0.7.0", 39 | "react-native-shimmer-placeholder": "git+https://github.com/tomzaku/react-native-shimmer-placeholder.git#expo", 40 | "react-native-svg": "12.1.0", 41 | "react-native-swiper": "^1.6.0", 42 | "react-native-web": "~0.11.7", 43 | "react-native-webview": "9.4.0", 44 | "react-navigation-shared-element": "^5.0.0-alpha1" 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "^7.8.6", 48 | "babel-preset-expo": "~8.1.0" 49 | }, 50 | "private": true 51 | } 52 | -------------------------------------------------------------------------------- /preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingki/sehatnegeriku/b5e1115be4c5c17ecbb51f52529d6e27c68ca7b9/preview.gif -------------------------------------------------------------------------------- /privacy-policy.md: -------------------------------------------------------------------------------- 1 | **Privacy Policy** 2 | 3 | Nur Fikri built the Sehat Negeriku Rebuild app as an Open Source app. This SERVICE is provided by Nur Fikri at no cost and is intended for use as is. 4 | 5 | This page is used to inform visitors regarding my policies with the collection, use, and disclosure of Personal Information if anyone decided to use my Service. 6 | 7 | If you choose to use my Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that I collect is used for providing and improving the Service. I will not use or share your information with anyone except as described in this Privacy Policy. 8 | 9 | The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which is accessible at Sehat Negeriku Rebuild unless otherwise defined in this Privacy Policy. 10 | 11 | **Information Collection and Use** 12 | 13 | For a better experience, while using our Service, I may require you to provide us with certain personally identifiable information, including but not limited to Nur FIkri. The information that I request will be retained on your device and is not collected by me in any way. 14 | 15 | The app does use third party services that may collect information used to identify you. 16 | 17 | Link to privacy policy of third party service providers used by the app 18 | 19 | - [Google Play Services](https://www.google.com/policies/privacy/) 20 | - [Expo](https://expo.io/privacy) 21 | 22 | **Log Data** 23 | 24 | I want to inform you that whenever you use my Service, in a case of an error in the app I collect data and information (through third party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing my Service, the time and date of your use of the Service, and other statistics. 25 | 26 | **Cookies** 27 | 28 | Cookies are files with a small amount of data that are commonly used as anonymous unique identifiers. These are sent to your browser from the websites that you visit and are stored on your device's internal memory. 29 | 30 | This Service does not use these “cookies” explicitly. However, the app may use third party code and libraries that use “cookies” to collect information and improve their services. You have the option to either accept or refuse these cookies and know when a cookie is being sent to your device. If you choose to refuse our cookies, you may not be able to use some portions of this Service. 31 | 32 | **Service Providers** 33 | 34 | I may employ third-party companies and individuals due to the following reasons: 35 | 36 | - To facilitate our Service; 37 | - To provide the Service on our behalf; 38 | - To perform Service-related services; or 39 | - To assist us in analyzing how our Service is used. 40 | 41 | I want to inform users of this Service that these third parties have access to your Personal Information. The reason is to perform the tasks assigned to them on our behalf. However, they are obligated not to disclose or use the information for any other purpose. 42 | 43 | **Security** 44 | 45 | I value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and I cannot guarantee its absolute security. 46 | 47 | **Links to Other Sites** 48 | 49 | This Service may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by me. Therefore, I strongly advise you to review the Privacy Policy of these websites. I have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services. 50 | 51 | **Children’s Privacy** 52 | 53 | These Services do not address anyone under the age of 13. I do not knowingly collect personally identifiable information from children under 13\. In the case I discover that a child under 13 has provided me with personal information, I immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact me so that I will be able to do necessary actions. 54 | 55 | **Changes to This Privacy Policy** 56 | 57 | I may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new Privacy Policy on this page. 58 | 59 | This policy is effective as of 2020-09-17 60 | 61 | **Contact Us** 62 | 63 | If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact me at codingki@gmail.com. 64 | -------------------------------------------------------------------------------- /screens/Detail.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { 3 | Text, 4 | View, 5 | StyleSheet, 6 | Dimensions, 7 | TouchableOpacity, 8 | ScrollView, 9 | Image, 10 | } from 'react-native'; 11 | import AsyncStorage from '@react-native-community/async-storage'; 12 | import { Icon } from '@ui-kitten/components'; 13 | import News from '../components/DetailNews'; 14 | import HTML from 'react-native-render-html'; 15 | import moment from 'moment'; 16 | import { SharedElement } from 'react-navigation-shared-element'; 17 | const { width } = Dimensions.get('window'); 18 | 19 | function Detail({ navigation, route }) { 20 | const { id, category, data } = route.params; 21 | const [content] = useState(data); 22 | const [saved, setSaved] = useState(false); 23 | 24 | useEffect(() => { 25 | checkSaved(); 26 | }, []); 27 | 28 | async function checkSaved() { 29 | try { 30 | const value = await AsyncStorage.getItem('saved:' + id); 31 | if (value !== null) { 32 | // We have data!! 33 | setSaved(true); 34 | } else { 35 | setSaved(false); 36 | } 37 | } catch (error) { 38 | setSaved(false); 39 | } 40 | } 41 | 42 | async function save() { 43 | if (content) { 44 | try { 45 | await AsyncStorage.setItem('saved:' + id, JSON.stringify(content)); 46 | setSaved(true); 47 | } catch (error) { 48 | alert(error); 49 | } 50 | } 51 | } 52 | 53 | async function remove() { 54 | if (content) { 55 | try { 56 | await AsyncStorage.removeItem('saved:' + id); 57 | setSaved(false); 58 | } catch (error) { 59 | alert(error); 60 | } 61 | } 62 | } 63 | function check() { 64 | if (data.thumbnail_images) { 65 | if (data.thumbnail_images.full) { 66 | if (data.thumbnail_images.full.url) { 67 | return true; 68 | } 69 | } 70 | } 71 | } 72 | return ( 73 | 74 | 81 | 91 | { 93 | navigation.goBack(); 94 | }} 95 | style={{ flex: 1, alignItems: 'flex-start' }} 96 | > 97 | 98 | 99 | 100 | 18 ? 18 : 0.0437 * width, 105 | }} 106 | > 107 | {category} 108 | 109 | 110 | { 117 | if (saved) { 118 | remove(); 119 | } else { 120 | save(); 121 | } 122 | }} 123 | > 124 | 130 | 131 | 132 | 133 | 134 | 135 | {check() ? ( 136 | 137 | 144 | 145 | ) : ( 146 | 147 | )} 148 | 149 | 159 | {data.title} 160 | 161 | {data.author.name} |{' '} 162 | {moment(data.date).format('dddd, Do MMMM YYYY HH mm')} 163 | 164 | 165 | 174 | 183 | 184 | 185 | 186 | ); 187 | } 188 | 189 | const styles = StyleSheet.create({ 190 | container: { 191 | flex: 1, 192 | backgroundColor: '#f5f5f5', 193 | }, 194 | }); 195 | 196 | Detail.sharedElements = (route) => { 197 | const { data } = route.params; 198 | return [ 199 | { 200 | id: `data.${data.id}`, 201 | }, 202 | ]; 203 | }; 204 | 205 | export default Detail; 206 | -------------------------------------------------------------------------------- /screens/Home.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from 'react'; 2 | import { 3 | View, 4 | StyleSheet, 5 | ScrollView, 6 | Dimensions, 7 | RefreshControl, 8 | } from 'react-native'; 9 | import { Tab, TabView } from '@ui-kitten/components'; 10 | 11 | import TopNav from '../components/topnav/TopNav'; 12 | import Rubrik from './rubrik/Rubrik'; 13 | 14 | const { width } = Dimensions.get('window'); 15 | 16 | export default function HomeScreen({ navigation }) { 17 | const [refreshing, setRefreshing] = useState(false); 18 | const [selectedIndex, setSelectedIndex] = useState(0); 19 | 20 | const refresh = useRef(); 21 | 22 | function onRefresh() { 23 | setRefreshing(true); 24 | refresh.current.refresh(); 25 | } 26 | 27 | return ( 28 | 29 | 37 | } 38 | > 39 | {/* Top Nav */} 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {selectedIndex == 0 && ( 49 | 56 | )} 57 | {selectedIndex == 1 && ( 58 | 66 | )} 67 | {selectedIndex == 2 && ( 68 | 76 | )} 77 | {selectedIndex == 3 && ( 78 | 86 | )} 87 | {selectedIndex == 4 && ( 88 | 96 | )} 97 | 98 | 99 | ); 100 | } 101 | 102 | const styles = StyleSheet.create({ 103 | container: { 104 | flex: 1, 105 | backgroundColor: '#f5f5f5', 106 | }, 107 | newsCardContainer: { 108 | backgroundColor: 'white', 109 | borderColor: '#e5e5e5', 110 | borderTopWidth: 1, 111 | flex: 1, 112 | paddingHorizontal: 15, 113 | paddingVertical: 7.5, 114 | }, 115 | tabContainer: { 116 | minHeight: 64, 117 | }, 118 | }); 119 | -------------------------------------------------------------------------------- /screens/Media.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from 'react'; 2 | import { 3 | View, 4 | StyleSheet, 5 | ScrollView, 6 | Dimensions, 7 | RefreshControl, 8 | } from 'react-native'; 9 | import { Tab, TabView } from '@ui-kitten/components'; 10 | 11 | import TopNav from '../components/topnav/TopNav'; 12 | import Rubrik from './rubrik/Rubrik'; 13 | 14 | const { width } = Dimensions.get('window'); 15 | 16 | export default function MediaScreen({ navigation }) { 17 | const [refreshing, setRefreshing] = useState(false); 18 | const [selectedIndex, setSelectedIndex] = useState(0); 19 | const refresh = useRef(); 20 | 21 | function onRefresh() { 22 | setRefreshing(true); 23 | refresh.current.refresh(); 24 | } 25 | return ( 26 | 27 | 35 | } 36 | > 37 | {/* Top Nav */} 38 | 39 | 40 | 41 | 42 | 43 | {selectedIndex == 0 && ( 44 | 52 | )} 53 | {selectedIndex == 1 && ( 54 | 62 | )} 63 | 64 | 65 | ); 66 | } 67 | 68 | const styles = StyleSheet.create({ 69 | container: { 70 | flex: 1, 71 | backgroundColor: '#f5f5f5', 72 | }, 73 | newsCardContainer: { 74 | backgroundColor: 'white', 75 | borderColor: '#e5e5e5', 76 | borderTopWidth: 1, 77 | flex: 1, 78 | paddingHorizontal: 15, 79 | paddingVertical: 7.5, 80 | }, 81 | tabContainer: { 82 | minHeight: 64, 83 | }, 84 | }); 85 | -------------------------------------------------------------------------------- /screens/Saved.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { 3 | View, 4 | StyleSheet, 5 | ScrollView, 6 | Dimensions, 7 | RefreshControl, 8 | } from 'react-native'; 9 | import AsyncStorage from '@react-native-community/async-storage'; 10 | import TopNav from '../components/topnav/SavedTopNav'; 11 | import Saved from '../components/page/PageSaved'; 12 | 13 | export default function SavedScreen({ navigation }) { 14 | const [data, setData] = useState(null); 15 | const [refreshing, setRefreshing] = useState(false); 16 | useEffect(() => { 17 | getData(); 18 | }, []); 19 | 20 | async function getData() { 21 | let keys = []; 22 | 23 | keys = await AsyncStorage.getAllKeys(); 24 | if (keys.length == 0) { 25 | setData(null); 26 | } else { 27 | getMultiple(keys); 28 | } 29 | } 30 | async function getMultiple(keys) { 31 | const values = await AsyncStorage.multiGet(keys); 32 | 33 | setData(values); 34 | setRefreshing(false); 35 | } 36 | const onRefresh = React.useCallback(() => { 37 | setRefreshing(true); 38 | getData(); 39 | }, []); 40 | 41 | return ( 42 | 43 | 51 | } 52 | > 53 | {/* Top Nav */} 54 | 55 | 56 | 57 | 58 | ); 59 | } 60 | 61 | const styles = StyleSheet.create({ 62 | container: { 63 | flex: 1, 64 | backgroundColor: '#f5f5f5', 65 | }, 66 | newsCardContainer: { 67 | backgroundColor: 'white', 68 | borderColor: '#e5e5e5', 69 | borderTopWidth: 1, 70 | flex: 1, 71 | paddingHorizontal: 15, 72 | paddingVertical: 7.5, 73 | }, 74 | tabContainer: { 75 | minHeight: 64, 76 | }, 77 | }); 78 | -------------------------------------------------------------------------------- /screens/Search.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, StyleSheet, ScrollView, Dimensions } from 'react-native'; 3 | 4 | import TopNav from '../components/topnav/SearchTopNav'; 5 | import Search from './rubrik/Search'; 6 | 7 | export default function SearchScreen({ navigation, route }) { 8 | const { search } = route.params; 9 | return ( 10 | 11 | 12 | 13 | {/* Top Nav */} 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | const styles = StyleSheet.create({ 22 | container: { 23 | flex: 1, 24 | backgroundColor: '#f5f5f5', 25 | }, 26 | newsCardContainer: { 27 | backgroundColor: 'white', 28 | borderColor: '#e5e5e5', 29 | borderTopWidth: 1, 30 | flex: 1, 31 | paddingHorizontal: 15, 32 | paddingVertical: 7.5, 33 | }, 34 | tabContainer: { 35 | minHeight: 64, 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /screens/rubrik/Rubrik.js: -------------------------------------------------------------------------------- 1 | import React, { useState, forwardRef, useImperativeHandle } from 'react'; 2 | import { 3 | StyleSheet, 4 | Dimensions, 5 | View, 6 | Text, 7 | TouchableOpacity, 8 | } from 'react-native'; 9 | import { Query } from 'react-apollo'; 10 | import gql from 'graphql-tag'; 11 | 12 | import PageLoading from '../../components/loading/Page'; 13 | import Page from '../../components/page/Page'; 14 | const { width } = Dimensions.get('window'); 15 | 16 | const HomeScreen = forwardRef((props, ref) => { 17 | const [isFetched, setIsFetched] = useState(false); 18 | const [count, setCount] = useState(10); 19 | 20 | function recentQuery(page) { 21 | return ` 22 | query { 23 | posts(count:${count},page:${page}) { 24 | id, 25 | title, 26 | date, 27 | categories{ 28 | title 29 | }, 30 | author{ 31 | name 32 | }, 33 | content, 34 | thumbnail_images{ 35 | full{ 36 | url 37 | } 38 | } 39 | } 40 | } 41 | `; 42 | } 43 | 44 | function categoryQuery(page) { 45 | return ` 46 | query { 47 | category(count:${count}, slug:"${props.slug}", page:${page}) { 48 | id, 49 | title, 50 | date, 51 | categories{ 52 | title 53 | }, 54 | author{ 55 | name 56 | }, 57 | content, 58 | thumbnail_images{ 59 | full{ 60 | url 61 | } 62 | } 63 | } 64 | } 65 | 66 | `; 67 | } 68 | 69 | return ( 70 | <> 71 | 76 | {({ loading, error, data, refetch }) => { 77 | useImperativeHandle(ref, () => ({ 78 | refresh() { 79 | refetch(); 80 | }, 81 | })); 82 | if (loading) { 83 | setIsFetched(false); 84 | return ; 85 | } else if (error) { 86 | return ( 87 | 93 | 99 | Koneksi error 100 | 101 | 102 | ); 103 | } else if (data) { 104 | props.setRefreshing(false); 105 | setIsFetched(true); 106 | return ( 107 | 113 | ); 114 | } 115 | }} 116 | 117 | { 129 | setCount(count + 10); 130 | }} 131 | > 132 | Load more 133 | 134 | 135 | ); 136 | }); 137 | 138 | export default HomeScreen; 139 | -------------------------------------------------------------------------------- /screens/rubrik/Search.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | StyleSheet, 4 | Dimensions, 5 | TouchableOpacity, 6 | Text, 7 | View, 8 | } from 'react-native'; 9 | import { Query } from 'react-apollo'; 10 | import gql from 'graphql-tag'; 11 | 12 | import PageLoading from '../../components/loading/LoadingSearch'; 13 | import Page from '../../components/page/PageSearch'; 14 | const { width } = Dimensions.get('window'); 15 | 16 | export default function HomeScreen(props) { 17 | const [isFetched, setIsFetched] = useState(false); 18 | const [count, setCount] = useState(10); 19 | 20 | function recentQuery(search, page) { 21 | return ` 22 | query { 23 | search(search:"${search}",count:${count},page:${page}) { 24 | id, 25 | title, 26 | date, 27 | categories{ 28 | title 29 | }, 30 | author{ 31 | name 32 | }, 33 | content, 34 | thumbnail_images{ 35 | full{ 36 | url 37 | } 38 | } 39 | } 40 | } 41 | `; 42 | } 43 | 44 | return ( 45 | <> 46 | 51 | {({ loading, error, data, refetch }) => { 52 | if (loading) { 53 | setIsFetched(false); 54 | return ; 55 | } else if (error) { 56 | return ( 57 | 63 | 69 | Koneksi error 70 | 71 | 72 | ); 73 | } else if (data) { 74 | setIsFetched(true); 75 | return ( 76 | 81 | ); 82 | } 83 | }} 84 | 85 | { 97 | setCount(count + 10); 98 | }} 99 | > 100 | Load more 101 | 102 | 103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /ss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingki/sehatnegeriku/b5e1115be4c5c17ecbb51f52529d6e27c68ca7b9/ss.png -------------------------------------------------------------------------------- /term-of-service.md: -------------------------------------------------------------------------------- 1 | **Terms & Conditions** 2 | This app is not the official app of the Sehat Negeriku KemenKes. 3 | 4 | By downloading or using the app, these terms will automatically apply to you – you should make sure therefore that you read them carefully before using the app. You’re not allowed to copy, or modify the app, any part of the app, or our trademarks in any way. You’re not allowed to attempt to extract the source code of the app, and you also shouldn’t try to translate the app into other languages, or make derivative versions. The app itself, and all the trade marks, copyright, database rights and other intellectual property rights related to it, still belong to Nur Fikri. 5 | 6 | Nur Fikri is committed to ensuring that the app is as useful and efficient as possible. For that reason, we reserve the right to make changes to the app or to charge for its services, at any time and for any reason. We will never charge you for the app or its services without making it very clear to you exactly what you’re paying for. 7 | 8 | The Sehat Negeriku Rebuild app stores and processes personal data that you have provided to us, in order to provide my Service. It’s your responsibility to keep your phone and access to the app secure. We therefore recommend that you do not jailbreak or root your phone, which is the process of removing software restrictions and limitations imposed by the official operating system of your device. It could make your phone vulnerable to malware/viruses/malicious programs, compromise your phone’s security features and it could mean that the Sehat Negeriku Rebuild app won’t work properly or at all. 9 | 10 | The app does use third party services that declare their own Terms and Conditions. 11 | 12 | Link to Terms and Conditions of third party service providers used by the app 13 | 14 | - [Google Play Services](https://policies.google.com/terms) 15 | - [Expo](https://expo.io/terms) 16 | 17 | You should be aware that there are certain things that Nur Fikri will not take responsibility for. Certain functions of the app will require the app to have an active internet connection. The connection can be Wi-Fi, or provided by your mobile network provider, but Nur Fikri cannot take responsibility for the app not working at full functionality if you don’t have access to Wi-Fi, and you don’t have any of your data allowance left. 18 | 19 | If you’re using the app outside of an area with Wi-Fi, you should remember that your terms of the agreement with your mobile network provider will still apply. As a result, you may be charged by your mobile provider for the cost of data for the duration of the connection while accessing the app, or other third party charges. In using the app, you’re accepting responsibility for any such charges, including roaming data charges if you use the app outside of your home territory (i.e. region or country) without turning off data roaming. If you are not the bill payer for the device on which you’re using the app, please be aware that we assume that you have received permission from the bill payer for using the app. 20 | 21 | Along the same lines, Nur Fikri cannot always take responsibility for the way you use the app i.e. You need to make sure that your device stays charged – if it runs out of battery and you can’t turn it on to avail the Service, Nur Fikri cannot accept responsibility. 22 | 23 | With respect to Nur Fikri’s responsibility for your use of the app, when you’re using the app, it’s important to bear in mind that although we endeavour to ensure that it is updated and correct at all times, we do rely on third parties to provide information to us so that we can make it available to you. Nur Fikri accepts no liability for any loss, direct or indirect, you experience as a result of relying wholly on this functionality of the app. 24 | 25 | At some point, we may wish to update the app. The app is currently available on Android – the requirements for system(and for any additional systems we decide to extend the availability of the app to) may change, and you’ll need to download the updates if you want to keep using the app. Nur Fikri does not promise that it will always update the app so that it is relevant to you and/or works with the Android version that you have installed on your device. However, you promise to always accept updates to the application when offered to you, We may also wish to stop providing the app, and may terminate use of it at any time without giving notice of termination to you. Unless we tell you otherwise, upon any termination, (a) the rights and licenses granted to you in these terms will end; (b) you must stop using the app, and (if needed) delete it from your device. 26 | 27 | **Changes to This Terms and Conditions** 28 | 29 | I may update our Terms and Conditions from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new Terms and Conditions on this page. 30 | 31 | These terms and conditions are effective as of 2020-09-17 32 | 33 | **Contact Us** 34 | 35 | If you have any questions or suggestions about my Terms and Conditions, do not hesitate to contact me at codingki@gmail.com. 36 | -------------------------------------------------------------------------------- /thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingki/sehatnegeriku/b5e1115be4c5c17ecbb51f52529d6e27c68ca7b9/thumbnail.jpg --------------------------------------------------------------------------------