diff --git a/app/_layout.tsx b/app/_layout.tsx index 301c00e7..9b704cb1 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -6,7 +6,13 @@ import { useTheme, } from "@react-navigation/native"; import { useFonts } from "expo-font"; -import { Stack, useLocalSearchParams, usePathname, useRouter, useSegments } from "expo-router"; +import { + Stack, + useLocalSearchParams, + usePathname, + useRouter, + useSegments, +} from "expo-router"; import * as SplashScreen from "expo-splash-screen"; import { useEffect } from "react"; @@ -27,6 +33,7 @@ import { AutocompleteDropdownContextProvider } from "react-native-autocomplete-d import { BrandContextProvider } from "@/contexts/brand-context.provider"; import CustomPaperTheme from "@/constants/Themes/Theme"; import { queryParams } from "@/utils/url"; +import { BottomSheetModalProvider } from "@gorhom/bottom-sheet"; export { // Catch any errors thrown by the Layout component. @@ -64,21 +71,23 @@ export default function RootLayout() { return ( - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + ); } @@ -119,9 +128,7 @@ const RootLayoutStack = () => { return ( - + { gestureEnabled: true, }} /> - + diff --git a/components/FilterModal.tsx b/components/FilterModal.tsx index 62ff54bf..fa2fea6b 100644 --- a/components/FilterModal.tsx +++ b/components/FilterModal.tsx @@ -7,7 +7,7 @@ import BottomSheet, { } from "@gorhom/bottom-sheet"; import { useTheme } from "@react-navigation/native"; import MultiSlider from "@ptomasroos/react-native-multi-slider"; -import { faClose } from "@fortawesome/free-solid-svg-icons"; +import { faCheckDouble, faClose } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"; import { stylesFn } from "@/styles/FilterModal.styles"; @@ -131,6 +131,13 @@ const CollaborationFilter = ({ {influencerType.map((category, index) => ( localInfluencerType === category ? ( + + ) : null} key={index} selected={localInfluencerType === category} onPress={() => setLocalInfluencerType(category)} diff --git a/components/InfluencerCard.tsx b/components/InfluencerCard.tsx index 3dab5314..d910615a 100644 --- a/components/InfluencerCard.tsx +++ b/components/InfluencerCard.tsx @@ -30,27 +30,21 @@ import { faEllipsis, faPeopleRoof, faChartLine, faFaceSmile, faComment, faCheck, import { imageUrl } from "@/utils/url"; import Tag from "./ui/tag"; import { MediaItem } from "./ui/carousel/render-media-item"; +import { User } from "@/types/User"; +import { processRawAttachment } from "@/utils/attachments"; +import RenderHTML from "react-native-render-html"; +import { truncateText } from "@/utils/text"; const { width } = Dimensions.get("window"); interface InfluencerCardPropsType { - influencer: { - id: string; - name: string; - handle: string; - profilePic: string; - media: MediaItem[]; - followers: number | string; - reach: number | string; - rating: number | string; - bio: string; - jobsCompleted: number | string; - successRate: number | string; - }; + influencer: User; type: string; alreadyInvited?: (influencerId: string) => Promise; ToggleModal: () => void; ToggleMessageModal?: () => void; + openProfile?: (influencer: User) => void; + setSelectedInfluencer?: React.Dispatch>; } const InfluencerCard = (props: InfluencerCardPropsType) => { @@ -68,6 +62,8 @@ const InfluencerCard = (props: InfluencerCardPropsType) => { const theme = useTheme(); const styles = stylesFn(theme); + const screenWidth = Dimensions.get("window").width; + const pinchHandler = useAnimatedGestureHandler({ onStart: (event) => { @@ -120,14 +116,30 @@ const InfluencerCard = (props: InfluencerCardPropsType) => { styles.header, ]} > - - + { + if (props.openProfile) { + props.openProfile(influencer); + } + }} + > + + + { + if (props.openProfile) { + props.openProfile(influencer); + } + }} + > {influencer.name} - {influencer.handle} - + {influencer.socials?.[0] || 'influencer-handle'} + {props.type === "invitation" && (isInvited ? ( { { props.ToggleModal(); + if (props?.setSelectedInfluencer) { + props.setSelectedInfluencer(props.influencer); + } }} > { processRawAttachment(attachment)) || []} onImagePress={onImagePress} /> @@ -189,7 +204,7 @@ const InfluencerCard = (props: InfluencerCardPropsType) => { size={20} /> - {convertToKUnits(Number(influencer.followers))} + {convertToKUnits(Number(influencer.backend?.followers))} @@ -199,7 +214,7 @@ const InfluencerCard = (props: InfluencerCardPropsType) => { size={20} /> - {convertToKUnits(Number(influencer.reach))} + {convertToKUnits(Number(influencer.backend?.reach))} @@ -209,7 +224,7 @@ const InfluencerCard = (props: InfluencerCardPropsType) => { size={20} /> - {influencer.rating} + {influencer.backend?.rating} @@ -222,11 +237,31 @@ const InfluencerCard = (props: InfluencerCardPropsType) => { - setBioExpanded(!bioExpanded)}> - - {influencer.bio} + { + if (props.openProfile) { + props.openProfile(influencer); + } + }} + > + + No content available.

", + }} + defaultTextProps={{ + style: { + color: Colors(theme).text, + fontSize: 16, + lineHeight: 22, + }, + }} + />
-
+ { @@ -234,8 +269,9 @@ const InfluencerCard = (props: InfluencerCardPropsType) => { }} > - {influencer.jobsCompleted} Jobs completed ({influencer.successRate}{" "} - success rate) + {/* {influencer.jobsCompleted} Jobs completed ({influencer.successRate}{" "} + success rate) */} + 10 Jobs completed 100% success rate diff --git a/components/collaboration/collaboration-details/ApplicationsTabContent.tsx b/components/collaboration/collaboration-details/ApplicationsTabContent.tsx index f42eb0ab..34b4a1cd 100644 --- a/components/collaboration/collaboration-details/ApplicationsTabContent.tsx +++ b/components/collaboration/collaboration-details/ApplicationsTabContent.tsx @@ -1,8 +1,5 @@ import React, { useEffect, useState } from "react"; -import { View, FlatList } from "react-native"; -import { - ActivityIndicator, -} from "react-native-paper"; +import { View, FlatList, ActivityIndicator } from "react-native"; import { useTheme } from "@react-navigation/native"; import InfluencerCard from "@/components/InfluencerCard"; @@ -11,7 +8,6 @@ import { collection, doc, getDoc, getDocs } from "firebase/firestore"; import BottomSheetActions from "@/components/BottomSheetActions"; import EmptyState from "@/components/ui/empty-state"; import Colors from "@/constants/Colors"; -import { processRawAttachment } from "@/utils/attachments"; import { useBreakpoints } from "@/hooks"; const ApplicationsTabContent = (props: any) => { @@ -75,7 +71,7 @@ const ApplicationsTabContent = (props: any) => { gap: 40, }} > - + ); } @@ -97,19 +93,7 @@ const ApplicationsTabContent = (props: any) => { renderItem={({ item }) => ( processRawAttachment(media)), - followers: item.backend ? item.backend.followers : 1000, - reach: item.backend ? item.backend.reach : 10000, - rating: item.backend ? item.backend.rating : 4.5, - bio: "I am a content creator", - jobsCompleted: 12, - successRate: 100, - }} + influencer={item} ToggleModal={() => { setIsVisible(true); setSelectedApplication({ diff --git a/components/collaboration/collaboration-details/InvitationsTabContent.tsx b/components/collaboration/collaboration-details/InvitationsTabContent.tsx index d6a615d8..bdfaabb0 100644 --- a/components/collaboration/collaboration-details/InvitationsTabContent.tsx +++ b/components/collaboration/collaboration-details/InvitationsTabContent.tsx @@ -117,22 +117,15 @@ const InvitationsTabContent = (props: any) => { type="invitation" alreadyInvited={checkIfAlreadyInvited} influencer={{ - id: item.id, - profilePic: item.profileImage, - name: item.name, - handle: item.handle || "@handle", - media: [ - { - type: "image", - url: item.profileImage, - }, - ], - followers: item.backend ? item.backend.followers : 1000, - reach: item.backend ? item.backend.reach : 10000, - rating: item.backend ? item.backend.rating : 4.5, - bio: "I am a content creator", - jobsCompleted: 12, - successRate: 100, + ...item, + profile: { + attachments: [ + { + type: "image", + imageUrl: item.profileImage, + }, + ] + } }} ToggleModal={() => { setIsVisible(true); diff --git a/components/explore-influencers/index.tsx b/components/explore-influencers/index.tsx index bb937ed5..3b70f4a2 100644 --- a/components/explore-influencers/index.tsx +++ b/components/explore-influencers/index.tsx @@ -1,15 +1,29 @@ -import { FlatList } from "react-native"; +import { ActivityIndicator, FlatList } from "react-native"; import InfluencerCard from "../InfluencerCard"; import { View } from "../theme/Themed"; import AppLayout from "@/layouts/app-layout"; import SearchComponent from "../SearchComponent"; -import { useState } from "react"; -import BottomSheetActions from "../BottomSheetActions"; -import { influencers } from "@/constants/Influencers"; +import { useEffect, useMemo, useRef, useState } from "react"; import CollaborationFilter from "../FilterModal"; import Colors from "@/constants/Colors"; import { useTheme } from "@react-navigation/native"; import { useBreakpoints } from "@/hooks"; +import { User } from "@/types/User"; + +import ProfileBottomSheet from "@/shared-uis/components/ProfileModal/Profile-Modal"; +import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from "@gorhom/bottom-sheet"; + +import { FirestoreDB } from "@/utils/firestore"; +import { + collection, + onSnapshot, + query, + where, +} from "firebase/firestore"; +import BottomSheetContainer from "../ui/bottom-sheet/BottomSheet"; +import { List } from "react-native-paper"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useSharedValue } from "react-native-reanimated"; const ExploreInfluencers = () => { const [searchQuery, setSearchQuery] = useState(""); @@ -25,40 +39,124 @@ const ExploreInfluencers = () => { const [currentEngagementRange, setCurrentEngagementRange] = useState([ 0, 10000000, ]); - const ToggleModal = () => setIsModalVisible(!isModalVisible); + const ToggleModal = () => { + setIsModalVisible(!isModalVisible); + }; + const [influencers, setInfluencers] = useState([]); + const [filteredInfluencers, setFilteredInfluencers] = useState([]); + const [selectedInfluencer, setSelectedInfluencer] = useState(null); + + const bottomSheetModalRef = useRef(null); + const snapPoints = useMemo(() => ["25%", "50%", "90%"], []); + + const insets = useSafeAreaInsets(); + const containerOffset = useSharedValue({ + top: insets.top, + bottom: insets.bottom, + left: insets.left, + right: insets.right, + }); + + const [isLoading, setIsLoading] = useState(true); + const theme = useTheme(); const { xl, } = useBreakpoints(); - const filteredInfluencers = influencers.filter((influencer) => { - const isCollaborationTypeMatch = - currentCollaborationType === "All" || - influencer.collaborationType === currentCollaborationType; - - const isFollowersRangeMatch = - Number(influencer.followers) >= currentFollowersRange[0] && - Number(influencer.followers) <= currentFollowersRange[1]; - const isReachRangeMatch = - Number(influencer.reach) >= currentReachRange[0] && - Number(influencer.reach) <= currentReachRange[1]; - const isEngagementRangeMatch = - Number(influencer.engagement) >= currentEngagementRange[0] && - Number(influencer.engagement) <= currentEngagementRange[1]; - - const isSearchQueryMatch = influencer.name - .toLowerCase() - .includes(searchQuery.toLowerCase()); + useEffect(() => { + setIsLoading(true); + const influencersRef = collection(FirestoreDB, "users"); + const q = query(influencersRef, where("profile.completionPercentage", ">=", 60)); + + const unsubscribe = onSnapshot(q, (querySnapshot) => { + const fetchedInfluencers: User[] = []; + querySnapshot.forEach((doc) => { + fetchedInfluencers.push({ + ...doc.data(), + id: doc.id, + } as User); + }); + + setInfluencers(fetchedInfluencers); + setIsLoading(false); + }); + + return () => { + unsubscribe(); + }; + }, []); + + const filterInfluencers = () => { + const newFilteredInfluencers = influencers.filter((influencer) => { + const isCollaborationTypeMatch = + currentCollaborationType === "All" || + influencer.profile?.category?.includes(currentCollaborationType); + + // const isFollowersRangeMatch = + // Number(influencer.backend?.followers) >= currentFollowersRange[0] && + // Number(influencer.backend?.followers) <= currentFollowersRange[1]; + // const isReachRangeMatch = + // Number(influencer.backend?.reach) >= currentReachRange[0] && + // Number(influencer.backend?.reach) <= currentReachRange[1]; + // const isEngagementRangeMatch = + // Number(influencer.backend?.engagement) >= currentEngagementRange[0] && + // Number(influencer.backend?.engagement) <= currentEngagementRange[1]; + + const isSearchQueryMatch = influencer.name + .toLowerCase() + .includes(searchQuery.toLowerCase()); + + return ( + isCollaborationTypeMatch && + // isFollowersRangeMatch && + // isReachRangeMatch && + // isEngagementRangeMatch && + isSearchQueryMatch + ); + }); + + setFilteredInfluencers(newFilteredInfluencers); + }; + useEffect(() => { + filterInfluencers(); + }, [ + currentCollaborationType, + currentInfluencerType, + currentFollowersRange, + currentReachRange, + currentEngagementRange, + searchQuery, + influencers, + ]); + + const renderBackdrop = (props: any) => { return ( - isCollaborationTypeMatch && - isFollowersRangeMatch && - isReachRangeMatch && - isEngagementRangeMatch && - isSearchQueryMatch + ); - }); + }; + + if (isLoading) { + return ( + + + + + + ); + } return ( @@ -70,6 +168,11 @@ const ExploreInfluencers = () => { type="explore" ToggleModal={ToggleModal} influencer={item} + openProfile={(influencer) => { + setSelectedInfluencer(influencer); + bottomSheetModalRef.current?.present(); + }} + setSelectedInfluencer={setSelectedInfluencer} /> )} keyExtractor={(item, index) => index.toString()} @@ -105,32 +208,74 @@ const ExploreInfluencers = () => { marginHorizontal: "auto", }} /> - {isModalVisible && ( - - )} - {isFilterModalVisible && ( - setIsFilterModalVisible(false)} - collaborationType={["All", "Paid", "Unpaid"]} - influencerType={["All", "Micro", "Macro"]} - setCollaborationType={setCurrentCollaborationType} - setInfluencerType={setCurrentInfluencerType} - setCurrentFollowersRange={setCurrentFollowersRange} - setCurrentReachRange={setCurrentReachRange} - setCurrentEngagementRange={setCurrentEngagementRange} - currentCollaborationType="All" - currentInfluencerType="All" - currentFollowersRange={[0, 1000000]} - currentReachRange={[0, 1000000]} - currentEngagementRange={[0, 100000]} - /> - )} + + { + isModalVisible && ( + + + { + bottomSheetModalRef.current?.present(); + ToggleModal(); + }} + /> + null} + /> + null} + /> + + + ) + } + + { + isFilterModalVisible && ( + setIsFilterModalVisible(false)} + collaborationType={["All", "Paid", "Unpaid"]} + influencerType={["All", "Micro", "Macro"]} + setCollaborationType={setCurrentCollaborationType} + setInfluencerType={setCurrentInfluencerType} + setCurrentFollowersRange={setCurrentFollowersRange} + setCurrentReachRange={setCurrentReachRange} + setCurrentEngagementRange={setCurrentEngagementRange} + currentCollaborationType="All" + currentInfluencerType="All" + currentFollowersRange={[0, 1000000]} + currentReachRange={[0, 1000000]} + currentEngagementRange={[0, 100000]} + /> + ) + } + + + + + + ); }; diff --git a/components/ui/bottom-sheet/BottomSheet.tsx b/components/ui/bottom-sheet/BottomSheet.tsx new file mode 100644 index 00000000..d1beedc9 --- /dev/null +++ b/components/ui/bottom-sheet/BottomSheet.tsx @@ -0,0 +1,87 @@ +import React, { PropsWithChildren } from "react"; +import { + Modal, + Pressable, + StyleSheet, + View, +} from "react-native"; +import BottomSheet, { BottomSheetView } from "@gorhom/bottom-sheet"; +import { Theme, useTheme } from "@react-navigation/native"; + +interface BottomSheetContainerProps extends PropsWithChildren { + isVisible: boolean; + snapPointsRange: [string, string]; + onClose: () => void; +} + +const BottomSheetContainer: React.FC = ({ + isVisible, + snapPointsRange, + onClose, + children, +}) => { + const sheetRef = React.useRef(null); + + const theme = useTheme(); + const styles = stylesFn(theme); + + const snapPoints = React.useMemo( + () => [snapPointsRange[0], snapPointsRange[1]], + [] + ); + + const handleClose = () => { + if (sheetRef.current) { + sheetRef.current.close(); + } + onClose(); + }; + + return ( + + + { + return ; + }} + onClose={handleClose} + style={styles.bottomSheet} + > + + {children} + + + + + ); +}; + +export default BottomSheetContainer; + +const stylesFn = (theme: Theme) => StyleSheet.create({ + overlay: { + position: "absolute", + top: 0, + bottom: 0, + left: 0, + right: 0, + backgroundColor: "rgba(0, 0, 0, 0.5)", + }, + bottomSheetContainer: { + flex: 1, + justifyContent: "flex-end", + zIndex: 2, + }, + bottomSheet: { + zIndex: 9999, + }, +}); diff --git a/constants/Influencer.ts b/constants/Influencer.ts new file mode 100644 index 00000000..e0154243 --- /dev/null +++ b/constants/Influencer.ts @@ -0,0 +1,56 @@ +import { IUsers } from "@/shared-libs/firestore/trendly-pro/models/users"; + +export const DUMMY_INFLUENCER: IUsers = { + name: "John Doe", + profileImage: + "https://www.pngkey.com/png/full/114-1149878_setting-user-avatar-in-specific-size-without-breaking.png", + email: "johndoe@gmail.com", + phoneNumber: "1234567890", + location: "New York", + emailVerified: true, + phoneVerified: true, + profile: { + introVideo: "", + content: { + about: "I am a social media influencer", + socialMediaHighlight: "I have 100k followers on Instagram", + collaborationGoals: "I want to collaborate with brands", + audienceInsights: "My audience is mostly from the US", + funFactAboutUser: "I love to travel", + }, + category: ["Fashion", "Lifestyle"], + attachments: [ + { + type: "image", + imageUrl: + "https://www.pngkey.com/png/full/114-1149878_setting-user-avatar-in-specific-size-without-breaking.png", + }, + ], + }, + preferences: { + question1: ["Option1"], + question2: ["Option2"], + question3: ["Option3"], + question4: ["Option4"], + }, + settings: { + theme: "light", + emailNotification: true, + pushNotification: true, + }, + backend: { + followers: 100000, + reach: 1000000, + engagement: 10, + rating: 4.5, + }, + pushNotificationToken: { + ios: [], + android: [], + web: [], + }, + //@ts-ignore + notifications: [], + //@ts-ignore + socials: [], +}; diff --git a/package-lock.json b/package-lock.json index 310ccaae..163bade6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "react-native-paper": "^5.12.5", "react-native-reanimated": "~3.10.1", "react-native-reanimated-carousel": "^3.5.1", + "react-native-render-html": "^6.3.4", "react-native-safe-area-context": "^4.10.5", "react-native-screens": "^3.31.1", "react-native-select-dropdown": "^4.0.1", @@ -6348,6 +6349,23 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsamr/counter-style": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@jsamr/counter-style/-/counter-style-2.0.2.tgz", + "integrity": "sha512-2mXudGVtSzVxWEA7B9jZLKjoXUeUFYDDtFrQoC0IFX9/Dszz4t1vZOmafi3JSw/FxD+udMQ+4TAFR8Qs0J3URQ==", + "license": "MIT" + }, + "node_modules/@jsamr/react-native-li": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@jsamr/react-native-li/-/react-native-li-2.3.1.tgz", + "integrity": "sha512-Qbo4NEj48SQ4k8FZJHFE2fgZDKTWaUGmVxcIQh3msg5JezLdTMMHuRRDYctfdHI6L0FZGObmEv3haWbIvmol8w==", + "license": "MIT", + "peerDependencies": { + "@jsamr/counter-style": "^1.0.0 || ^2.0.0", + "react": "*", + "react-native": "*" + } + }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -6363,6 +6381,92 @@ "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", "dev": true }, + "node_modules/@native-html/css-processor": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@native-html/css-processor/-/css-processor-1.11.0.tgz", + "integrity": "sha512-NnhBEbJX5M2gBGltPKOetiLlKhNf3OHdRafc8//e2ZQxXN8JaSW/Hy8cm94pnIckQxwaMKxrtaNT3x4ZcffoNQ==", + "license": "MIT", + "dependencies": { + "css-to-react-native": "^3.0.0", + "csstype": "^3.0.8" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-native": "*" + } + }, + "node_modules/@native-html/transient-render-engine": { + "version": "11.2.3", + "resolved": "https://registry.npmjs.org/@native-html/transient-render-engine/-/transient-render-engine-11.2.3.tgz", + "integrity": "sha512-zXwgA3gPUEmFs3I3syfnvDvS6WiUHXEE6jY09OBzK+trq7wkweOSFWIoyXiGkbXrozGYG0KY90YgPyr8Tg8Uyg==", + "license": "MIT", + "dependencies": { + "@native-html/css-processor": "1.11.0", + "@types/ramda": "^0.27.44", + "csstype": "^3.0.9", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.2", + "domutils": "^2.8.0", + "htmlparser2": "^7.1.2", + "ramda": "^0.27.2" + }, + "peerDependencies": { + "@types/react-native": "*", + "react-native": "^*" + } + }, + "node_modules/@native-html/transient-render-engine/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/@native-html/transient-render-engine/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/@native-html/transient-render-engine/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/@native-html/transient-render-engine/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -10670,6 +10774,15 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" }, + "node_modules/@types/ramda": { + "version": "0.27.66", + "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.27.66.tgz", + "integrity": "sha512-i2YW+E2U6NfMt3dp0RxNcejox+bxJUNDjB7BpYuRuoHIzv5juPHkJkNgcUOu+YSQEmaWu8cnAo/8r63C0NnuVA==", + "license": "MIT", + "dependencies": { + "ts-toolbelt": "^6.15.1" + } + }, "node_modules/@types/react": { "version": "18.2.79", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.79.tgz", @@ -10679,6 +10792,31 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-native": { + "version": "0.72.8", + "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.72.8.tgz", + "integrity": "sha512-St6xA7+EoHN5mEYfdWnfYt0e8u6k2FR0P9s2arYgakQGFgU1f9FlPrIEcj0X24pLCF5c5i3WVuLCUdiCYHmOoA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@react-native/virtualized-lists": "^0.72.4", + "@types/react": "*" + } + }, + "node_modules/@types/react-native/node_modules/@react-native/virtualized-lists": { + "version": "0.72.8", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.72.8.tgz", + "integrity": "sha512-J3Q4Bkuo99k7mu+jPS9gSUSgq+lLRSI/+ahXNwV92XgJ/8UgOTxu2LPwhJnBk/sQKxq7E8WkZBnBiozukQMqrw==", + "license": "MIT", + "peer": true, + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "peerDependencies": { + "react-native": "*" + } + }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -10699,6 +10837,12 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true }, + "node_modules/@types/urijs": { + "version": "1.19.25", + "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.25.tgz", + "integrity": "sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "7.4.7", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", @@ -12521,6 +12665,15 @@ "node": ">=6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001642", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001642.tgz", @@ -12601,6 +12754,26 @@ "node": ">=10" } }, + "node_modules/character-entities-html4": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.4.tgz", + "integrity": "sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", @@ -13427,6 +13600,15 @@ "node": ">=8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, "node_modules/css-in-js-utils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", @@ -13451,6 +13633,17 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css-tree": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", @@ -16562,6 +16755,89 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/htmlparser2": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz", + "integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.2", + "domutils": "^2.8.0", + "entities": "^3.0.1" + } + }, + "node_modules/htmlparser2/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -23015,6 +23291,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ramda": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.2.tgz", + "integrity": "sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==", + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -23362,6 +23644,27 @@ "react-native-reanimated": ">=3.0.0" } }, + "node_modules/react-native-render-html": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/react-native-render-html/-/react-native-render-html-6.3.4.tgz", + "integrity": "sha512-H2jSMzZjidE+Wo3qCWPUMU1nm98Vs2SGCvQCz/i6xf0P3Y9uVtG/b0sDbG/cYFir2mSYBYCIlS1Dv0WC1LjYig==", + "license": "BSD-2-Clause", + "dependencies": { + "@jsamr/counter-style": "^2.0.1", + "@jsamr/react-native-li": "^2.3.0", + "@native-html/transient-render-engine": "11.2.3", + "@types/ramda": "^0.27.40", + "@types/urijs": "^1.19.15", + "prop-types": "^15.5.7", + "ramda": "^0.27.2", + "stringify-entities": "^3.1.0", + "urijs": "^1.19.6" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-safe-area-context": { "version": "4.10.5", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.10.5.tgz", @@ -25647,6 +25950,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-3.1.0.tgz", + "integrity": "sha512-3FP+jGMmMV/ffZs86MoghGqAoqXAdxLrJP4GUdrDN1aIScYih5tuIO3eF4To5AJZ79KDZ8Fpdy7QJnK8SsL1Vg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -26484,6 +26802,12 @@ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, + "node_modules/ts-toolbelt": { + "version": "6.15.5", + "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-6.15.5.tgz", + "integrity": "sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==", + "license": "Apache-2.0" + }, "node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", @@ -26838,6 +27162,12 @@ "punycode": "^2.1.0" } }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "license": "MIT" + }, "node_modules/url": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", diff --git a/package.json b/package.json index 59e5e0b6..80e0f7a2 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "react-native-paper": "^5.12.5", "react-native-reanimated": "~3.10.1", "react-native-reanimated-carousel": "^3.5.1", + "react-native-render-html": "^6.3.4", "react-native-safe-area-context": "^4.10.5", "react-native-screens": "^3.31.1", "react-native-select-dropdown": "^4.0.1", diff --git a/styles/InfluencerCard.styles.tsx b/styles/InfluencerCard.styles.tsx index 79de1619..0848e91f 100644 --- a/styles/InfluencerCard.styles.tsx +++ b/styles/InfluencerCard.styles.tsx @@ -88,10 +88,6 @@ export const stylesFn = (theme: Theme) => flexDirection: "row", justifyContent: "space-between", }, - bio: { - fontSize: 14, - color: Colors(theme).text, - }, jobHistory: { color: Colors(theme).primary, textDecorationLine: "underline", diff --git a/utils/conversion.ts b/utils/conversion.ts index 435dce58..20b9fe89 100644 --- a/utils/conversion.ts +++ b/utils/conversion.ts @@ -1,4 +1,8 @@ export const convertToKUnits = (num: number) => { + if (Number.isNaN(num)) { + return 0; + } + if (num >= 1000) { return `${(num / 1000).toFixed(1)}k`; } diff --git a/utils/text.ts b/utils/text.ts new file mode 100644 index 00000000..9845aef3 --- /dev/null +++ b/utils/text.ts @@ -0,0 +1,5 @@ +export const truncateText = (text: string, length: number) => { + if (!text) return ""; + + return text.length > length ? text.slice(0, length) + "..." : text; +};