Prechádzať zdrojové kódy

News Detail Page Implementation

Savio Fernando 1 rok pred
rodič
commit
71df279495

+ 2 - 1
POC_Documentation.md

@@ -9,4 +9,5 @@
     <a>https://blog.logrocket.com/adding-custom-fonts-react-native/</a>
 5. Setup React-native-linear-gradient
     <a>https://blog.logrocket.com/complex-gradients-react-native-linear-gradient/</a>
-6. Setup Experimental Android Animation on Project
+6. Setup Experimental Android Animation on Project
+7. Use React Native Bottom Sheet for Bottom sheet Modals

+ 23 - 0
components/atoms/ButtonWrapper.js

@@ -0,0 +1,23 @@
+import { StyleSheet, Text, View, TouchableWithoutFeedback} from 'react-native'
+import React from 'react'
+
+const ButtonWrapper = ({ onPress, children, buttonStyle}) => {
+    return (
+        <TouchableWithoutFeedback onPress={onPress}>
+            <View style={[styles.buttonStyle, buttonStyle]}>
+                {children}
+            </View>
+        </TouchableWithoutFeedback>
+    )
+}
+
+export default ButtonWrapper
+
+const styles = StyleSheet.create({
+    buttonStyle: {
+        alignItems: 'center',
+        justifyContent: 'center',
+        height: 'auto',
+        width: 'auto'
+    }, 
+})

+ 2 - 2
components/molecules/SearchTextInput.js

@@ -4,13 +4,13 @@ import { TextInput } from 'react-native-paper'
 import colors from '../../theme/colors'
 import fonts from '../../theme/fonts'
 
-const SearchTextInput = () => {
+const SearchTextInput = ({placeholder}) => {
   return (
     <View style={styles.container}>
       <TextInput
         editable
         mode='outlined'
-        placeholder='Which article you would like to see'
+        placeholder={placeholder ?? 'Which article you would like to see'}
         placeholderTextColor={colors.lightGray}
         dense
         style={{

+ 20 - 11
components/organisms/Buttons/BookmarkButton.js

@@ -1,20 +1,29 @@
-import { StyleSheet, Text, View } from 'react-native'
+import { StyleSheet} from 'react-native'
 import React from 'react'
-import { IconButton } from 'react-native-paper'
 import colors from '../../../theme/colors'
 import fonts from '../../../theme/fonts'
-
-const BookmarkButton = ({ size, onPress}) => {
+import IonIcon from 'react-native-vector-icons/Ionicons'
+import ButtonWrapper from '../../atoms/ButtonWrapper'
+const BookmarkButton = ({ buttonStyle,iconSize,iconColor, onPress}) => {
     return (
-        <IconButton
-            icon='bookmark-outline'
-            iconColor={colors.topColor}
-            size={size ?? fonts.getSize(24)}
-            onPress={onPress}
-        />
+        <ButtonWrapper onPress={onPress} buttonStyle={buttonStyle}>
+            <IonIcon name='ios-bookmark-outline' color={iconColor ? iconColor : styles.icon.color} size={iconSize ? iconSize : styles.icon.fontSize}/>
+        </ButtonWrapper>
     )
 }
 
 export default BookmarkButton
 
-// const styles = StyleSheet.create({})
+const styles = StyleSheet.create({
+    buttonStyle:{
+        alignItems: 'center',
+        justifyContent: 'center',
+        height:'auto',
+        width:'auto'
+    },
+    icon:{
+        color: colors.topColor,
+        fontSize: fonts.getSize(16)
+    }
+
+})

+ 20 - 10
components/organisms/Buttons/ShareButton.js

@@ -1,20 +1,30 @@
-import { StyleSheet, Text, View } from 'react-native'
+import { StyleSheet, } from 'react-native'
 import React from 'react'
-import { IconButton } from 'react-native-paper'
 import colors from '../../../theme/colors'
 import fonts from '../../../theme/fonts'
+import IonIcon from 'react-native-vector-icons/Ionicons'
+import ButtonWrapper from '../../atoms/ButtonWrapper'
 
-const ShareButton = ({size, onPress}) => {
+const ShareButton = ({ size, buttonStyle, iconColor, iconSize, onPress }) => {
   return (
-    <IconButton
-        icon='share-outline'
-        iconColor={colors.topColor}
-        size={size ?? fonts.getSize(24)}
-        onPress={onPress}
-    />
+    <ButtonWrapper onPress={onPress} buttonStyle={buttonStyle}>
+      <IonIcon name='share-social-outline' color={iconColor ? iconColor : styles.icon.color} size={iconSize ? iconSize : styles.icon.fontSize} />
+    </ButtonWrapper>
   )
 }
 
 export default ShareButton
 
-// const styles = StyleSheet.create({})
+const styles = StyleSheet.create({
+  buttonStyle: {
+    alignItems: 'center',
+    justifyContent: 'center',
+    height: 'auto',
+    width: 'auto'
+  },
+  icon: {
+    color: colors.topColor,
+    fontSize: fonts.getSize(16)
+  }
+
+})

+ 170 - 0
components/organisms/Cards/CommentCard.js

@@ -0,0 +1,170 @@
+import { Image, StyleSheet, Text, View } from 'react-native'
+import React, { useState } from 'react'
+import AntIcon from 'react-native-vector-icons/AntDesign'
+import ButtonWrapper from '../../atoms/ButtonWrapper'
+import LineIcon from 'react-native-vector-icons/SimpleLineIcons'
+import colors from '../../../theme/colors'
+import fonts from '../../../theme/fonts'
+import images from '../../../assets/images/images'
+import { Menu, PaperProvider } from 'react-native-paper'
+
+const CommentCard = ({ navigation, timestamp, name, comment, likeCount, dislikeCount, }) => {
+
+    // * Like Counter
+    const [isDisliked, setDisliked] = useState(false)
+    const [isLiked, setLiked] = useState(false)
+    const [likeCounter, setLikeCounter] = useState(likeCount ?? 123)
+    const [dislikeCounter, setDislikeCounter] = useState(dislikeCount ?? 452)
+
+    // * Menu Visibility
+    const [menuVisible, setMenuVisible] = React.useState(false);
+    const openMenu = () => setMenuVisible(true);
+    const closeMenu = () => setMenuVisible(false);
+
+    const menuAnchor = () => <ButtonWrapper onPress={openMenu} buttonStyle={styles.buttonStyle}>
+        <LineIcon name='options-vertical' color={styles.icon.color} size={styles.icon.fontSize} />
+    </ButtonWrapper>
+
+    const handleToggleDislike = () => {
+        setDisliked(!isDisliked)
+        setDislikeCounter(isDisliked === true ? dislikeCounter - 1 : dislikeCounter + 1)
+    }
+
+    const handleToggleLike = () => {
+        setLiked(!isLiked)
+        setLikeCounter(isLiked === true ? likeCounter - 1 : likeCounter + 1)
+    }
+
+
+
+    return (
+        <View style={styles.commentContainer}>
+            <View style={styles.header}>
+                <View style={{ flexDirection: 'row' }}>
+                    <Image source={images.imageCard} style={styles.profileIcon} />
+                    <View style={{ justifyContent: 'center' }}>
+                        <Text style={styles.name}>Semina Gurung</Text>
+                        <Text style={styles.timestamp}>2 days ago</Text>
+                    </View>
+                </View>
+
+                <PaperProvider>
+                    <Menu
+                        visible={menuVisible}
+                        onDismiss={closeMenu}
+                    // anchor={
+                    //     menuAnchor()
+                    // }>
+                    >
+                        <Menu.Item onPress={() => { }} title="Item 1" />
+                        <Menu.Item onPress={() => { }} title="Item 2" />
+                        <Menu.Item onPress={() => { }} title="Item 3" />
+                        
+                    </Menu>
+                    <ButtonWrapper onPress={openMenu} buttonStyle={styles.buttonStyle}>
+                            <LineIcon name='options-vertical' color={styles.icon.color} size={styles.icon.fontSize} />
+                        </ButtonWrapper>
+                </PaperProvider>
+
+            </View>
+            <Text style={{ paddingVertical: fonts.getSize(13), fontFamily: fonts.type.regular, fontSize: fonts.getSize(11) }}>
+                Lorem Ip sum is simply dummy text of the printing and typesetting industry. Lorem Ipsum is simply du
+            </Text>
+            <View style={styles.header}>
+                <View style={{ flexDirection: 'row', alignItems: 'center' }}>
+                    <View style={{ flexDirection: 'row', paddingRight: fonts.getSize(16) }}>
+                        <Image source={images.imageCard} style={styles.replyIcon} />
+                        <Image source={images.imageCard} style={styles.replyIcon} />
+                        <Image source={images.imageCard} style={styles.replyIcon} />
+                    </View>
+                    <Text style={styles.utilText}>View 14 replies</Text>
+                </View>
+                <View style={styles.utilContainer}>
+                    <ButtonWrapper
+                        buttonStyle={styles.utilButtons}
+                        onPress={() => {
+                            handleToggleLike()
+                        }}
+                    >
+                        <Text style={styles.utilText}>{`${likeCounter}`}</Text>
+                        <AntIcon name={isLiked ? "like1" : "like2"} color={styles.icon.color} size={16} />
+                    </ButtonWrapper>
+                    <ButtonWrapper
+                        buttonStyle={styles.utilButtons}
+                        onPress={() => {
+                            handleToggleDislike()
+                        }}
+                    >
+                        <Text style={styles.utilText}>{`${dislikeCounter}`}</Text>
+                        <AntIcon name={isDisliked ? "dislike1" : "dislike2"} color={styles.icon.color} size={16} />
+                    </ButtonWrapper>
+                </View>
+            </View>
+        </View>
+    )
+}
+
+export default CommentCard
+
+const styles = StyleSheet.create({
+    header: {
+        flexDirection: 'row',
+        alignItems: 'center',
+        justifyContent: 'space-between'
+    },
+    commentContainer: {
+        width: '100%',
+        height: 150,
+        // height: 'auto',
+        paddingVertical: fonts.getSize(4)
+    },
+    profileIcon: {
+        height: 42,
+        width: 42,
+        borderRadius: 42,
+        marginRight: fonts.getSize(8),
+
+    },
+    buttonStyle: {
+        alignItems: 'center',
+        justifyContent: 'center',
+        height: 'auto',
+        width: 'auto'
+    },
+    icon: {
+        color: colors.topColor,
+        fontSize: fonts.getSize(16)
+    },
+    name: {
+        fontFamily: fonts.type.regular,
+        color: colors.black,
+        fontSize: fonts.getSize(12)
+    },
+    timestamp: {
+        fontFamily: fonts.type.regular,
+        color: colors.gray,
+        fontSize: fonts.getSize(9)
+    },
+    replyIcon: {
+        height: 22,
+        width: 22,
+        borderColor: colors.white,
+        borderWidth: 1,
+        borderRadius: 24,
+        marginRight: -8
+    },
+    utilContainer: {
+        flexDirection: 'row',
+        gap: 12
+    },
+    utilButtons: {
+        flexDirection: 'row',
+        alignItems: 'baseline'
+    },
+    utilText: {
+        fontFamily: fonts.type.medium,
+        color: colors.black,
+        paddingRight: 4
+    },
+
+})

+ 3 - 0
components/organisms/Cards/HorizontalNewsCardVariant.js

@@ -111,6 +111,9 @@ const styles = StyleSheet.create({
   },
   utilButtons: {
     flexDirection: 'row',
+    padding: fonts.getSize(8),
+    width:64,
+    justifyContent:'space-between'
     //  padding:0
     // alignItems: 'flex-start',
     // justifyContent: 'flex-end',

+ 14 - 0
components/organisms/Sections/CommentSection.js

@@ -0,0 +1,14 @@
+import { StyleSheet, Text, View } from 'react-native'
+import React from 'react'
+
+const CommentSection = () => {
+  return (
+    <View>
+      <Text>CommentSection</Text>
+    </View>
+  )
+}
+
+export default CommentSection
+
+const styles = StyleSheet.create({})

+ 17 - 0
package-lock.json

@@ -1579,6 +1579,23 @@
       "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==",
       "dev": true
     },
+    "@gorhom/bottom-sheet": {
+      "version": "4.4.7",
+      "resolved": "https://registry.npmjs.org/@gorhom/bottom-sheet/-/bottom-sheet-4.4.7.tgz",
+      "integrity": "sha512-ukTuTqDQi2heo68hAJsBpUQeEkdqP9REBcn47OpuvPKhdPuO1RBOOADjqXJNCnZZRcY+HqbnGPMSLFVc31zylQ==",
+      "requires": {
+        "@gorhom/portal": "1.0.14",
+        "invariant": "^2.2.4"
+      }
+    },
+    "@gorhom/portal": {
+      "version": "1.0.14",
+      "resolved": "https://registry.npmjs.org/@gorhom/portal/-/portal-1.0.14.tgz",
+      "integrity": "sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A==",
+      "requires": {
+        "nanoid": "^3.3.1"
+      }
+    },
     "@hapi/hoek": {
       "version": "9.3.0",
       "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",

+ 1 - 0
package.json

@@ -10,6 +10,7 @@
     "test": "jest"
   },
   "dependencies": {
+    "@gorhom/bottom-sheet": "^4.4.7",
     "@react-navigation/bottom-tabs": "^6.5.7",
     "@react-navigation/drawer": "^6.6.3",
     "@react-navigation/native": "^6.1.6",

+ 226 - 15
screens/NewsDetailPage.js

@@ -1,26 +1,237 @@
-import { StyleSheet, Text, View, TouchableWithoutFeedback } from 'react-native'
-import React from 'react'
+import { StyleSheet, Text, View, TouchableWithoutFeedback, ScrollView, Image } from 'react-native'
+import React, { useRef, useCallback, useMemo } from 'react'
 import Header from '../components/atoms/Header'
 import FeatherIcon from 'react-native-vector-icons/Feather'
 import MaterialIcon from 'react-native-vector-icons/MaterialIcons'
 import colors from '../theme/colors'
+import fonts from '../theme/fonts'
+import images from '../assets/images/images'
+import IonIcon from 'react-native-vector-icons/Ionicons'
+import BookmarkButton from '../components/organisms/Buttons/BookmarkButton'
+import ShareButton from '../components/organisms/Buttons/ShareButton'
+import {
+    BottomSheetModal,
+    BottomSheetModalProvider,
+    BottomSheetView,
+    BottomSheetScrollView
+} from '@gorhom/bottom-sheet';
+import ButtonWrapper from '../components/atoms/ButtonWrapper'
+import CommentCard from '../components/organisms/Cards/CommentCard'
+
+
+
+
+const NewsDetailPage = ({ navigation, headline, tagline, timestamp, author, newsText, image, comments }) => {
+
+    const commentsData = [{},{},{},{},{}]
+    const bottomSheetModalRef = useRef('');
+
+    // Variables
+    const snapPoints = useMemo(() => ['70%', '100%'], []);
+
+
+    // Callbacks
+    const handlePresentModalPress = useCallback(() => {
+        bottomSheetModalRef.current?.present();
+    }, []);
+
+    const handleCloseModalPress = () => bottomSheetModalRef.current.close();
+
+    const handleSheetChanges = useCallback((index) => {
+        console.log('handleSheetChanges', index);
+    }, []);
+
 
-const NewsDetailPage = ({ navigation }) => {
     return (
-        <View>
-            <Header>
-                <TouchableWithoutFeedback onPress={() => {navigation.goBack()}}>
-                    <FeatherIcon name={'chevron-left'} size={24} color={colors.black} />
-                </TouchableWithoutFeedback>
-                <TouchableWithoutFeedback onPress={() => navigation.toggleDrawer()}>
-                                    <MaterialIcon name='list' color={colors.topColor} size={30} />
-                </TouchableWithoutFeedback>
-
-            </Header>
-        </View>
+
+        <BottomSheetModalProvider>
+            <ScrollView>
+                <View style={styles.container}>
+                    <Header>
+                        <TouchableWithoutFeedback onPress={() => { navigation.goBack() }}>
+                            <FeatherIcon name={'chevron-left'} size={24} color={colors.black} />
+                        </TouchableWithoutFeedback>
+                        <TouchableWithoutFeedback onPress={() => navigation.toggleDrawer()}>
+                            <MaterialIcon name='list' color={colors.topColor} size={30} />
+                        </TouchableWithoutFeedback>
+                    </Header>
+                    <View style={styles.newsContainer}>
+                        <Text style={styles.headline}>
+                            {headline ?? "ICICI Securities sees over 49% upside potential on this chemical stock"}
+                        </Text>
+                        <Text style={styles.tagline}>
+                            {tagline ?? "The reason behind theirent is that iPhone users have been The reason behind theirent is that iPhone.."}
+                        </Text>
+                        <View style={styles.descriptors}>
+                            <Text style={styles.descriptorText}>{timestamp ?? "15 min ago"}</Text>
+                            <Text style={styles.descriptorText}>{author ?? "By Lucy Hiddleston"}</Text>
+                        </View>
+                        {/* Test Card */}
+                        <View style={styles.imagesContainer}>
+                            <Image source={image ?? images.verticalCard} style={styles.image} />
+                        </View>
+                        <View style={styles.utilButtons}>
+                            <TouchableWithoutFeedback onPress={handlePresentModalPress}>
+                                <View style={styles.commentSection}>
+                                    <IonIcon name="chatbubble-outline" size={fonts.getSize(20)} color={colors.topColor} />
+                                    <Text style={styles.commentText}>{comments ?? 123} COMMENTS</Text>
+                                </View>
+                            </TouchableWithoutFeedback>
+                            <View style={{ flexDirection: 'row' }}>
+                                <BookmarkButton
+                                    buttonStyle={styles.buttonStyle}
+                                    iconSize={20}
+                                    onPress={true}
+                                />
+                                <ShareButton
+                                    buttonStyle={styles.buttonStyle}
+                                    iconSize={20}
+                                    onPress={true}
+                                />
+                            </View>
+                        </View>
+                        <Text style={styles.newsText}>
+                            {newsText ?? "Samsung had a pretty quiet Mobile World Congress event, but it did tell us we’d learn more about its upcoming Google-approved smartwatch at its next Unpacked event. Unfortunately, the company didn’t tell us when exactly that would be, but a new report from Korean publication DigitalDaily News (via 9to5Google) claims the next Unpacked will take place on August 11, at 10 AM ET."}
+                        </Text>
+                        <TouchableWithoutFeedback onPress={true}>
+                            <Text style={styles.backToTopText}>Back to Top</Text>
+                        </TouchableWithoutFeedback>
+                    </View>
+
+                    <>
+                        <BottomSheetModal
+                            ref={bottomSheetModalRef}
+                            index={1}
+                            snapPoints={snapPoints}
+                            onChange={handleSheetChanges}
+                        >
+                            <BottomSheetScrollView>
+                                <BottomSheetView style={{ paddingHorizontal: fonts.getSize(24) }}>
+                                    <BottomSheetView style={styles.commentInputContainer}>
+                                        <Text style={{ fontFamily: fonts.type.semibold, color: colors.black, fontSize: fonts.getSize(16) }}>
+                                            Comments
+                                        </Text>
+                                        <ButtonWrapper onPress={handleCloseModalPress}>
+                                            <IonIcon name='close-sharp' size={fonts.getSize(20)} color={colors.black} />
+                                        </ButtonWrapper>
+                                    </BottomSheetView>
+                                    <View style={styles.commentInput}>
+                                        <Image source={images.imageCard} style={[styles.profileImage]} />
+                                        <Text >Comment Text Input</Text>
+                                    </View>
+                                    <>
+                                        <Text style={{fontFamily:fonts.type.medium, color: colors.black,paddingBottom: fonts.getSize(8)}}>View all Comments(04)</Text>
+                                    </>
+                                    <BottomSheetView style={{gap:fonts.getSize(16)}}>
+                                       {commentsData.map(() => <CommentCard />)}
+                                    </BottomSheetView>
+
+                                </BottomSheetView>
+                            </BottomSheetScrollView>
+
+                        </BottomSheetModal>
+                    </>
+                </View>
+            </ScrollView>
+        </BottomSheetModalProvider>
+
+
+
     )
 }
 
 export default NewsDetailPage
 
-const styles = StyleSheet.create({})
+const styles = StyleSheet.create({
+    newsContainer: {
+        paddingHorizontal: fonts.getSize(24)
+    },
+    headline: {
+        fontFamily: fonts.type.semibold,
+        fontSize: fonts.getSize(18),
+        paddingVertical: fonts.getSize(8),
+        color: colors.black,
+
+    },
+    tagline: {
+        fontFamily: fonts.type.regular,
+        color: colors.gray
+    },
+    descriptors: {
+        flexDirection: 'row',
+        gap: fonts.getSize(16),
+    },
+    descriptorText: {
+        fontFamily: fonts.type.semibold,
+        fontSize: fonts.getSize(10),
+        color: colors.black,
+        paddingTop: fonts.getSize(8),
+        paddingBottom: fonts.getSize(16)
+    },
+    newsText: {
+        color: colors.gray,
+        fontFamily: fonts.type.regular,
+        lineHeight: fonts.getSize(24),
+        paddingVertical: fonts.getSize(4)
+    },
+    backToTopText: {
+        color: colors.topColor,
+        alignSelf: 'center',
+        justifyContent: 'center',
+        paddingVertical: fonts.getSize(16),
+        fontSize: fonts.getSize(16),
+        fontFamily: fonts.type.regular,
+        textDecorationStyle: 'solid',
+        textDecorationLine: 'underline'
+    },
+    imagesContainer: {
+        width: "auto",
+        height: fonts.getSize(196),
+        paddingHorizontal: 0
+    },
+    image: {
+        borderRadius: fonts.getSize(4),
+        width: '100%',
+        height: '100%'
+    },
+    utilButtons: {
+        flexDirection: 'row',
+        justifyContent: 'space-between',
+        paddingVertical: fonts.getSize(4)
+    },
+    buttonStyle: {
+        padding: fonts.getSize(8)
+    },
+    commentSection: {
+        alignItems: 'center',
+        justifyContent: 'center',
+        flexDirection: 'row',
+        gap: fonts.getSize(8),
+        paddingLeft: fonts.getSize(4)
+    },
+    commentText: {
+        fontFamily: fonts.type.regular,
+        color: colors.gray
+    },
+    commentInputContainer: {
+        flexDirection: 'row',
+        justifyContent: 'space-between',
+        alignItems: 'center', 
+        paddingVertical: fonts.getSize(4)
+
+    },
+    profileImage: {
+        height: 42,
+        width: 42,
+        borderRadius: 32,
+        marginRight: fonts.getSize(16)
+
+    },
+    commentInput: {
+        paddingVertical: fonts.getSize(16),
+        alignItems: 'center',
+        justifyContent: 'flex-start',
+        flexDirection: 'row'
+    },
+
+})