From 0775b96d1e85f3a0e52cfa7206f6a3aa6ae4a8b3 Mon Sep 17 00:00:00 2001 From: Manan Sharma <112090668+Manan-Sharma-5@users.noreply.github.com> Date: Thu, 2 Jan 2025 16:47:33 +0530 Subject: [PATCH 01/17] pushed new contract screens --- app/(main)/(drawer)/(tabs)/_layout.tsx | 56 ++- app/(main)/(drawer)/(tabs)/contracts.tsx | 37 ++ .../(drawer)/(tabs)/create-collaboration.tsx | 11 - .../contract-details/[pageID].tsx | 144 ++++++++ app/(main)/(screens)/(menu)/preferences.tsx | 4 +- .../brand-profile/members-card/index.tsx | 84 +++++ .../card-components/CollaborationDetails.tsx | 3 +- .../card-components/CollaborationStats.tsx | 1 - .../OverviewTabContent.tsx | 86 ++--- components/contract-card/ContractDetails.tsx | 69 ++++ components/contract-card/ContractHeader.tsx | 115 +++++++ components/contract-card/UserResponse.tsx | 155 +++++++++ components/contracts/ActionContainer.tsx | 323 ++++++++++++++++++ components/contracts/AddMemberModal.tsx | 205 +++++++++++ .../contracts/ContractDetailContent.tsx | 219 ++++++++++++ components/contracts/FeedbackModal.tsx | 212 ++++++++++++ components/contracts/MemberContainer.tsx | 107 ++++++ components/contracts/active.tsx | 236 +++++++++++++ components/contracts/past.tsx | 236 +++++++++++++ components/menu/index.tsx | 18 +- components/preferences/index.tsx | 105 +++++- components/profile/index.tsx | 55 +-- components/proposals/Applications.tsx | 2 +- components/ui/modal/ImageUploadModal.tsx | 57 +--- components/ui/modal/MembersModal.tsx | 94 +++++ .../ViewCollaborationMap.tsx | 112 ++++++ constants/Colors.ts | 6 +- contexts/chat-context.provider.native.tsx | 127 +++++-- contexts/chat-context.provider.web.tsx | 42 ++- package-lock.json | 8 +- package.json | 1 + styles/CollaborationDetails.styles.tsx | 5 +- styles/Members.tsx | 71 ++++ styles/menu/MenuItem.styles.tsx | 1 + styles/profile/ProfileItemCard.styles.tsx | 3 +- 35 files changed, 2796 insertions(+), 214 deletions(-) create mode 100644 app/(main)/(drawer)/(tabs)/contracts.tsx delete mode 100644 app/(main)/(drawer)/(tabs)/create-collaboration.tsx create mode 100644 app/(main)/(screens)/(contract-details)/contract-details/[pageID].tsx create mode 100644 components/brand-profile/members-card/index.tsx create mode 100644 components/contract-card/ContractDetails.tsx create mode 100644 components/contract-card/ContractHeader.tsx create mode 100644 components/contract-card/UserResponse.tsx create mode 100644 components/contracts/ActionContainer.tsx create mode 100644 components/contracts/AddMemberModal.tsx create mode 100644 components/contracts/ContractDetailContent.tsx create mode 100644 components/contracts/FeedbackModal.tsx create mode 100644 components/contracts/MemberContainer.tsx create mode 100644 components/contracts/active.tsx create mode 100644 components/contracts/past.tsx create mode 100644 components/ui/modal/MembersModal.tsx create mode 100644 components/view-collaboration/ViewCollaborationMap.tsx create mode 100644 styles/Members.tsx diff --git a/app/(main)/(drawer)/(tabs)/_layout.tsx b/app/(main)/(drawer)/(tabs)/_layout.tsx index 3f60454c..e124e077 100644 --- a/app/(main)/(drawer)/(tabs)/_layout.tsx +++ b/app/(main)/(drawer)/(tabs)/_layout.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { router, Tabs } from "expo-router"; +import { Tabs } from "expo-router"; import Colors from "@/constants/Colors"; import { useClientOnlyValue } from "@/components/useClientOnlyValue"; @@ -10,12 +10,13 @@ import { View } from "@/components/theme/Themed"; import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"; import { faComment, + faFileLines, faStar, } from "@fortawesome/free-regular-svg-icons"; import { faComment as faCommentSolid, faHouseUser as faHouseUserSolid, - faPlusCircle, + faFileLines as faFileLinesSolid, faStar as faStarSolid, } from "@fortawesome/free-solid-svg-icons"; import Header from "@/components/explore-influencers/header"; @@ -48,8 +49,8 @@ const TabLayout = () => { ( + title: "Explore", + tabBarIcon: ({ color, focused }) => focused ? ( { size={24} /> ) : ( - - ) - ), + + ), headerTitle() { - return ( -
- ) + return
; }, headerRight: () => ( { }} /> ( ), + title: "Messages", + headerTitleAlign: "left", headerRight: () => , }} /> ({ - tabPress: (e) => { - e.preventDefault(); - router.push({ - pathname: "/(modal)/create-collaboration", - }); - }, - })} + name="collaborations" options={{ - headerShown: false, - title: "Create Collaborations", - tabBarIcon: ({ color }) => ( + title: "Collaborations", + tabBarIcon: ({ color, focused }) => ( ), + headerRight: () => , }} /> ( ), - title: "Messages", - headerTitleAlign: "left", headerRight: () => , }} /> , }} /> diff --git a/app/(main)/(drawer)/(tabs)/contracts.tsx b/app/(main)/(drawer)/(tabs)/contracts.tsx new file mode 100644 index 00000000..9d5c2638 --- /dev/null +++ b/app/(main)/(drawer)/(tabs)/contracts.tsx @@ -0,0 +1,37 @@ +import { View } from "@/components/theme/Themed"; +import AppLayout from "@/layouts/app-layout"; +import Applications from "@/components/proposals/Applications"; +import Invitations from "@/components/proposals/Invitations"; +import TopTabNavigation from "@/components/ui/top-tab-navigation"; +import ActiveContracts from "@/components/contracts/active"; +import PastContracts from "@/components/contracts/past"; + +const tabs = [ + { + id: "Active", + title: "Active", + component: , + }, + { + id: "Past", + title: "Past", + component: , + }, +]; + +const Contracts = () => { + return ( + + + + + + ); +}; + +export default Contracts; diff --git a/app/(main)/(drawer)/(tabs)/create-collaboration.tsx b/app/(main)/(drawer)/(tabs)/create-collaboration.tsx deleted file mode 100644 index 3ed3d7d2..00000000 --- a/app/(main)/(drawer)/(tabs)/create-collaboration.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { useRouter } from "expo-router"; - -const CreateCollaborationScreen = () => { - const router = useRouter(); - - router.push("/(modal)/create-collaboration"); - - return null; -}; - -export default CreateCollaborationScreen; diff --git a/app/(main)/(screens)/(contract-details)/contract-details/[pageID].tsx b/app/(main)/(screens)/(contract-details)/contract-details/[pageID].tsx new file mode 100644 index 00000000..7f7c21bd --- /dev/null +++ b/app/(main)/(screens)/(contract-details)/contract-details/[pageID].tsx @@ -0,0 +1,144 @@ +import { useEffect, useState } from "react"; +import { IconButton } from "react-native-paper"; +import { useLocalSearchParams } from "expo-router"; + +import AppLayout from "@/layouts/app-layout"; +import BottomSheetActions from "@/components/BottomSheetActions"; +import CollaborationDetails from "@/components/collaboration/collaboration-details"; +import ScreenHeader from "@/components/ui/screen-header"; +import Colors from "@/constants/Colors"; +import { useTheme } from "@react-navigation/native"; +import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"; +import { faEllipsisV } from "@fortawesome/free-solid-svg-icons"; +import ContractDetailsContent from "@/components/contracts/ContractDetailContent"; +import { AuthApp } from "@/utils/auth"; +import { + collectionGroup, + doc, + getDocs, + getDoc, + query, + where, +} from "firebase/firestore"; +import { FirestoreDB } from "@/utils/firestore"; +import { IContracts } from "@/shared-libs/firestore/trendly-pro/models/contracts"; +import { IUsers } from "@/shared-libs/firestore/trendly-pro/models/users"; +import { + IApplications, + ICollaboration, +} from "@/shared-libs/firestore/trendly-pro/models/collaborations"; +import { View } from "@/components/theme/Themed"; +import { ActivityIndicator } from "react-native"; + +interface ICollaborationCard extends IContracts { + userData: IUsers; + applications: IApplications[]; + collaborationData: ICollaboration; +} + +const ContractScreen = () => { + const theme = useTheme(); + const [isVisible, setIsVisible] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const { pageID } = useLocalSearchParams(); + const [contract, setContract] = useState(); + + const fetchProposals = async () => { + try { + const user = AuthApp.currentUser; + + if (!user?.uid) { + throw new Error("User not authenticated"); + } + + const contractsCol = doc(FirestoreDB, "contracts", pageID as string); + const contractsSnapshot = await getDoc(contractsCol); + + const contract = contractsSnapshot.data() as IContracts; + const collaborationId = contract.collaborationId; + + const userDataRef = doc(FirestoreDB, "users", contract.userId); + const userSnapshot = await getDoc(userDataRef); + const userData = userSnapshot.data() as IUsers; + + const hasAppliedQuery = query( + collectionGroup(FirestoreDB, "applications"), + where("userId", "==", contract.userId), + where("collaborationId", "==", collaborationId) + ); + + const hasAppliedSnapshot = await getDocs(hasAppliedQuery); + + //@ts-ignore + const applications = hasAppliedSnapshot.docs.map((appDoc) => ({ + id: appDoc.id, + ...appDoc.data(), + })) as IApplications[]; + + const collaborationRef = doc( + FirestoreDB, + "collaborations", + collaborationId + ); + const collaborationSnapshot = await getDoc(collaborationRef); + const collaborationData = collaborationSnapshot.data() as ICollaboration; + + setContract({ + ...contract, + userData, + applications, + collaborationData, + }); + } catch (error) { + console.error("Error fetching proposals: ", error); + } + }; + + useEffect(() => { + fetchProposals(); + }, []); + + if (isLoading || !contract) { + return ( + + + + + + ); + } + + return ( + + ( + + )} + onPress={() => { + setIsVisible(true); + }} + iconColor={Colors(theme).text} + /> + } + /> + + + ); +}; + +export default ContractScreen; diff --git a/app/(main)/(screens)/(menu)/preferences.tsx b/app/(main)/(screens)/(menu)/preferences.tsx index 3bcc2fd9..b742e62f 100644 --- a/app/(main)/(screens)/(menu)/preferences.tsx +++ b/app/(main)/(screens)/(menu)/preferences.tsx @@ -6,9 +6,7 @@ import ScreenHeader from "@/components/ui/screen-header"; const PreferencesScreen = () => { return ( - + ); diff --git a/components/brand-profile/members-card/index.tsx b/components/brand-profile/members-card/index.tsx new file mode 100644 index 00000000..c85b3e56 --- /dev/null +++ b/components/brand-profile/members-card/index.tsx @@ -0,0 +1,84 @@ +import { ManagerCard } from "@/components/preferences"; +import { View, Text } from "@/components/theme/Themed"; +import Colors from "@/constants/Colors"; +import { IManagers } from "@/shared-libs/firestore/trendly-pro/models/managers"; +import { imageUrl } from "@/utils/url"; +import { faEllipsisH } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"; +import { useTheme } from "@react-navigation/native"; +import { FC } from "react"; +import { Avatar } from "react-native-paper"; + +interface MembersCardProps { + manager: ManagerCard; +} + +const MembersCard: FC = ({ manager }) => { + const theme = useTheme(); + + if (!manager) { + return null; + } + + return ( + + + + + + + {manager.name} + + + {manager.email} + + + + + + {manager.status === 0 ? ( + + Invite Sent + + ) : null} + + + + ); +}; + +export default MembersCard; diff --git a/components/collaboration-card/card-components/CollaborationDetails.tsx b/components/collaboration-card/card-components/CollaborationDetails.tsx index 6fabfe5b..53a0df47 100644 --- a/components/collaboration-card/card-components/CollaborationDetails.tsx +++ b/components/collaboration-card/card-components/CollaborationDetails.tsx @@ -43,7 +43,8 @@ const CollaborationDetails: FC = ({ return ( = ( ); const fetchInvite = await getDocs(invitedRef); const docsDataInvited = fetchInvite.docs.map((doc) => doc.data()); - console.log(docsDataInvited.length); setInvitedCount(docsDataInvited.length); }; diff --git a/components/collaboration/collaboration-details/OverviewTabContent.tsx b/components/collaboration/collaboration-details/OverviewTabContent.tsx index 84212ae6..b00cf1c5 100644 --- a/components/collaboration/collaboration-details/OverviewTabContent.tsx +++ b/components/collaboration/collaboration-details/OverviewTabContent.tsx @@ -26,12 +26,12 @@ import { faInstagram, faYoutube, } from "@fortawesome/free-brands-svg-icons"; -import CreateCollaborationMap from "../create-collaboration/CreateCollaborationMap"; import { IManagers } from "@/shared-libs/firestore/trendly-pro/models/managers"; import { PLACEHOLDER_IMAGE } from "@/constants/Placeholder"; import BrandModal from "./modal/BrandModal"; import ManagerModal from "./modal/ManagerModal"; import { PromotionType } from "@/shared-libs/firestore/trendly-pro/constants/promotion-type"; +import ViewCollaborationMap from "@/components/view-collaboration/ViewCollaborationMap"; interface CollaborationDetailsContentProps { collaboration: CollaborationDetail; @@ -106,7 +106,7 @@ const OverviewTabContent = (props: CollaborationDetailsContentProps) => { return ( {/* Collaboration Details */} - + {props?.collaboration?.attachments && props?.collaboration?.attachments.length > 0 && ( { - - {props.collaboration.name} - - {props.collaboration.timeStamp ? ( - - {formatDistanceToNow(props.collaboration.timeStamp, { - addSuffix: true, - })} + + + {props.collaboration.name} + + {props.collaboration.timeStamp ? ( + + {formatDistanceToNow(props.collaboration.timeStamp, { + addSuffix: true, + })} + + ) : null} + + + + {props.collaboration.description} - ) : null} + + - - {props.collaboration.description} - - - - { flexWrap: "wrap", overflow: "hidden", color: Colors(theme).text, + lineHeight: 22, }} > {truncateText(props.collaboration.brandDescription, 120)} @@ -204,7 +221,7 @@ const OverviewTabContent = (props: CollaborationDetailsContentProps) => { - + {props.collaboration?.externalLinks && props.collaboration?.externalLinks.length > 0 && ( @@ -213,7 +230,6 @@ const OverviewTabContent = (props: CollaborationDetailsContentProps) => { flexDirection: "row", flexWrap: "wrap", gap: 16, - marginTop: 16, justifyContent: "space-between", }} > @@ -222,7 +238,6 @@ const OverviewTabContent = (props: CollaborationDetailsContentProps) => { key={index} mode="contained" style={{ - marginBottom: 16, flexBasis: 1, flexGrow: 1, }} @@ -245,7 +260,6 @@ const OverviewTabContent = (props: CollaborationDetailsContentProps) => { borderWidth: 0.3, borderRadius: 10, padding: 16, - marginTop: 16, }} > { { {props.collaboration.contentFormat.map((content, index) => ( @@ -347,8 +358,6 @@ const OverviewTabContent = (props: CollaborationDetailsContentProps) => { { fontSize: 16, color: Colors(theme).text, fontWeight: "bold", + marginBottom: 16, }} > Location - { )} )} - + { - + = ({ application }) => { + const theme = useTheme(); + return ( + + + + {application.message} + + + + + Quote: Rs {application.quotation} + + + {/* timeline */} + Timeline: {new Date(application.timeline).toLocaleDateString()} + + + + ); +}; + +export default ContractDetails; diff --git a/components/contract-card/ContractHeader.tsx b/components/contract-card/ContractHeader.tsx new file mode 100644 index 00000000..af027d3d --- /dev/null +++ b/components/contract-card/ContractHeader.tsx @@ -0,0 +1,115 @@ +import { Text, View } from "@/components/theme/Themed"; +import { FC } from "react"; +import { formatDistanceToNow } from "date-fns"; +import { Avatar } from "react-native-paper"; +import { Image, Pressable } from "react-native"; +import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"; +import Colors from "@/constants/Colors"; +import { faEllipsisH } from "@fortawesome/free-solid-svg-icons"; +import { useTheme } from "@react-navigation/native"; +import { imageUrl } from "@/utils/url"; + +interface ContractHeaderProps { + username: string; + collabName: string; + userImage: string; + collabId: string; + timePosted?: number; + onOpenBottomSheet: () => void; +} + +const ContractHeader: FC = ({ + username, + collabName, + collabId, + userImage, + timePosted, + onOpenBottomSheet, +}) => { + const theme = useTheme(); + + return ( + + + + + + {username} + + + {collabName} + + + + + {timePosted ? ( + + {formatDistanceToNow(timePosted, { + addSuffix: true, + })} + + ) : null} + { + onOpenBottomSheet(); + }} + > + + + + + ); +}; + +export default ContractHeader; diff --git a/components/contract-card/UserResponse.tsx b/components/contract-card/UserResponse.tsx new file mode 100644 index 00000000..472f0c47 --- /dev/null +++ b/components/contract-card/UserResponse.tsx @@ -0,0 +1,155 @@ +import { IApplications } from "@/shared-libs/firestore/trendly-pro/models/collaborations"; +import { FC } from "react"; +import { Text, View } from "../theme/Themed"; +import { Button } from "react-native-paper"; +import { Pressable, ScrollView } from "react-native"; +import RenderMediaItem from "@/shared-uis/components/carousel/render-media-item"; +import { processRawAttachment } from "@/utils/attachments"; +import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"; +import { faPaperclip } from "@fortawesome/free-solid-svg-icons"; +import { formatDistanceToNow } from "date-fns"; +import { router } from "expo-router"; +import Colors from "@/constants/Colors"; +import { useTheme } from "@react-navigation/native"; + +interface UserResponseProps { + application?: IApplications; + influencerQuestions?: string[]; + setConfirmationModalVisible: (value: boolean) => void; +} + +const UserResponse: FC = ({ + application, + influencerQuestions, + setConfirmationModalVisible, +}) => { + const attachmentFiltered = application?.attachments.map((attachment) => { + return processRawAttachment(attachment); + }); + const theme = useTheme(); + + return ( + + + + Application + + + + + {attachmentFiltered?.map((attachment, index) => ( + {}} + /> + ))} + + + {application?.message} + + + + Quote: {application?.quotation || "N/A"} + + {application?.timeline && ( + + Timeline:{" "} + {new Date(application?.timeline).toLocaleDateString() || "N/A"} + + )} + + {application?.fileAttachments && + application.fileAttachments.map((attachment, index) => { + return ( + + + + {attachment.name} + + + ); + })} + + + {application?.answersFromInfluencer && + influencerQuestions && + application?.answersFromInfluencer.map((answer, index) => { + return ( + + + {influencerQuestions[answer.question]} + + + {answer.answer} + + + ); + })} + + + + ); +}; + +export default UserResponse; diff --git a/components/contracts/ActionContainer.tsx b/components/contracts/ActionContainer.tsx new file mode 100644 index 00000000..fe718b12 --- /dev/null +++ b/components/contracts/ActionContainer.tsx @@ -0,0 +1,323 @@ +import { useTheme } from "@react-navigation/native"; +import { Text, View } from "../theme/Themed"; +import { FC, useEffect, useState } from "react"; +import { Button } from "react-native-paper"; +import Colors from "@/constants/Colors"; +import React from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"; +import { faStar, faStarHalfStroke } from "@fortawesome/free-solid-svg-icons"; +import { faNoteSticky } from "@fortawesome/free-regular-svg-icons"; +import { IContracts } from "@/shared-libs/firestore/trendly-pro/models/contracts"; +import { doc, getDoc, updateDoc } from "firebase/firestore"; +import { FirestoreDB } from "@/utils/firestore"; +import { IManagers } from "@/shared-libs/firestore/trendly-pro/models/managers"; +import { Image } from "react-native"; +import { imageUrl } from "@/utils/url"; +import { IUsers } from "@/shared-libs/firestore/trendly-pro/models/users"; +import { useChatContext } from "@/contexts"; +import Toaster from "@/shared-uis/components/toaster/Toaster"; +import { router } from "expo-router"; + +interface ActionContainerProps { + contract: IContracts; + refreshData: () => void; + feedbackModalVisible: () => void; + userData: IUsers; +} + +const ActionContainer: FC = ({ + contract, + refreshData, + feedbackModalVisible, + userData, +}) => { + const theme = useTheme(); + const [manager, setManager] = useState(); + const { sendSystemMessage, fetchChannelCid } = useChatContext(); + + const renderStars = (rating: number) => { + const fullStars = Math.floor(rating); + const hasHalfStar = rating % 1 !== 0; + + return ( + <> + {Array.from({ length: fullStars }, (_, i) => ( + + ))} + {hasHalfStar && ( + + )} + + ); + }; + + const startContract = async () => { + const contractRef = doc(FirestoreDB, "contracts", contract.streamChannelId); + const timeStarted = new Date().getTime(); + await updateDoc(contractRef, { + status: 1, + contractTimestamp: { + startedOn: timeStarted, + }, + }).then(() => { + Toaster.success("Contract started"); + refreshData(); + }); + }; + + const endContract = async () => { + const contractRef = doc(FirestoreDB, "contracts", contract.streamChannelId); + const timeEnded = new Date().getTime(); + await updateDoc(contractRef, { + status: 2, + contractTimestamp: { + ...contract.contractTimestamp, + endedOn: timeEnded, + }, + }).then(() => { + Toaster.success("Contract ended"); + refreshData(); + }); + }; + + const fetchManager = async () => { + if (!contract.feedbackFromBrand?.managerId) return; + const managerRef = doc( + FirestoreDB, + "managers", + contract.feedbackFromBrand?.managerId + ); + const manager = await getDoc(managerRef); + setManager(manager.data() as IManagers); + }; + + useEffect(() => { + fetchManager(); + }, [contract.feedbackFromBrand?.managerId]); + + return ( + + + {contract.status === 0 && ( + <> + + + + )} + {contract.status === 1 && ( + <> + + + + )} + {contract.status === 2 && !contract.feedbackFromBrand && ( + <> + + + )} + + {contract.feedbackFromBrand && ( + + + {renderStars(contract.feedbackFromBrand.ratings || 0)} + + + + + + From Brand ({manager?.name}) + + + {contract.feedbackFromBrand.feedbackReview} + + + + + )} + {contract.feedbackFromInfluencer && ( + + + {renderStars(contract.feedbackFromInfluencer.ratings || 0)} + + + + + + From Influencer ({userData.name}) + + + {contract.feedbackFromInfluencer?.feedbackReview} + + + + + )} + + + + {contract.status === 0 + ? "Please make sure to use this chat to first understand the the influencer. Post that, you can start your collaboration here" + : contract.status === 1 + ? "Please note, if your collaboration is done, we would need you to close the collaboration here. Having open collaborations idle for a long time can end up reducing the rating" + : contract.status === 2 + ? "Feedbacks are important for us. Our platform works on what people give feedback to each other. You see that other persons feedback only if you give your feedback" + : "You can create new collaboration and invite user to collaboration"} + + + + ); +}; + +export default ActionContainer; diff --git a/components/contracts/AddMemberModal.tsx b/components/contracts/AddMemberModal.tsx new file mode 100644 index 00000000..cd17bfe8 --- /dev/null +++ b/components/contracts/AddMemberModal.tsx @@ -0,0 +1,205 @@ +import Colors from "@/constants/Colors"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"; +import { useTheme } from "@react-navigation/native"; +import React, { FC, useEffect, useState } from "react"; +import { + View, + StyleSheet, + FlatList, + Modal, + TouchableOpacity, + Pressable, +} from "react-native"; +import { Text, Button, Avatar, Card } from "react-native-paper"; +import { collection, doc, getDoc, getDocs } from "firebase/firestore"; +import { useBrandContext } from "@/contexts/brand-context.provider"; +import { FirestoreDB } from "@/utils/firestore"; +import { useChatContext } from "@/contexts"; +import { IUsers } from "@/shared-libs/firestore/trendly-pro/models/users"; +import { IBrandsMembers } from "@/shared-libs/firestore/trendly-pro/models/brands"; +import { IManagers } from "@/shared-libs/firestore/trendly-pro/models/managers"; +import Toaster from "@/shared-uis/components/toaster/Toaster"; + +interface AddMemberModalProps { + visible: boolean; + onDismiss: () => void; + membersAlreadyInContract: any[]; + channelId: string; + refreshData: () => void; + updateMemberContainer: () => void; +} + +const AddMembersModal: FC = ({ + visible, + onDismiss, + membersAlreadyInContract, + channelId, + refreshData, + updateMemberContainer, +}) => { + const [members, setMembers] = useState([]); + const theme = useTheme(); + const { addMemberToChannel } = useChatContext(); + const { selectedBrand } = useBrandContext(); + + const fetchMembersInBrand = async () => { + if (!selectedBrand) return; + try { + const memberRef = collection( + FirestoreDB, + "brands", + selectedBrand.id, + "members" + ); + const memberDoc = await getDocs(memberRef); + const membersData = memberDoc.docs.map((doc) => { + return { + ...doc.data(), + managerId: doc.id, + } as IBrandsMembers; + }); + + var members = await Promise.all( + membersData.map(async (member) => { + const memberDoc = doc(FirestoreDB, "managers", member.managerId); + const memberData = getDoc(memberDoc).then((doc) => { + return { + ...doc.data(), + managerId: doc.id, + status: member.status, + }; + }) as any; + return memberData; + }) + ); + + members = members.filter((member) => { + return !membersAlreadyInContract.some((memberInContract) => { + if (!memberInContract) return false; + return memberInContract.email === member.email; + }); + }); + + //@ts-ignore + setMembers(members); + } catch (error) { + console.error(error); + } + }; + + useEffect(() => { + fetchMembersInBrand(); + }, [membersAlreadyInContract]); + + const handleAddMember = (memberId: string) => { + try { + addMemberToChannel(channelId, memberId); + Toaster.success("Member added successfully"); + refreshData(); + onDismiss(); + setTimeout(() => { + updateMemberContainer(); + }, 1000); + } catch (error) { + Toaster.error("Failed to add member"); + console.error(error); + } + }; + + const renderItem = (item: any) => ( + + + + + + {item.item.name} + + + {item.item.email} + + + handleAddMember(item.item.managerId)}> + + + + + ); + + return ( + + + + Add Members + + Add your team members here + + + + + ); +}; + +const styles = StyleSheet.create({ + modalTitle: { + fontSize: 18, + fontWeight: "bold", + marginBottom: 8, + }, + card: { + marginBottom: 12, + elevation: 1, + }, + cardContent: { + flexDirection: "row", + alignItems: "center", + padding: 8, + }, + textContainer: { + flex: 1, + marginLeft: 12, + }, + addButton: { + alignSelf: "center", + }, +}); + +export default AddMembersModal; diff --git a/components/contracts/ContractDetailContent.tsx b/components/contracts/ContractDetailContent.tsx new file mode 100644 index 00000000..32d7a8f6 --- /dev/null +++ b/components/contracts/ContractDetailContent.tsx @@ -0,0 +1,219 @@ +import React, { useEffect, useState } from "react"; +import { View, ScrollView, Pressable, Dimensions } from "react-native"; +import { Text, Card, Paragraph, Button, Portal } from "react-native-paper"; +import { useTheme } from "@react-navigation/native"; +import { stylesFn } from "@/styles/CollaborationDetails.styles"; +import Colors from "@/constants/Colors"; +import Carousel from "@/shared-uis/components/carousel/carousel"; +import { processRawAttachment } from "@/utils/attachments"; +import { formatDistanceToNow } from "date-fns"; +import { + IApplications, + ICollaboration, +} from "@/shared-libs/firestore/trendly-pro/models/collaborations"; +import UserResponse from "@/components/contract-card/UserResponse"; +import RenderHTML from "react-native-render-html"; +import { IUsers } from "@/shared-libs/firestore/trendly-pro/models/users"; +import ActionContainer from "./ActionContainer"; +import { IContracts } from "@/shared-libs/firestore/trendly-pro/models/contracts"; +import MemberContainer from "./MemberContainer"; +import AddMembersModal from "./AddMemberModal"; +import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"; +import { faArrowRight } from "@fortawesome/free-solid-svg-icons"; +import FeedbackModal from "./FeedbackModal"; +import { router } from "expo-router"; + +interface CollaborationDetailsContentProps { + collaborationDetail: ICollaboration; + applicationData?: IApplications; + userData: IUsers; + contractData: IContracts; + refreshData: () => void; +} + +const ContractDetailsContent = (props: CollaborationDetailsContentProps) => { + const theme = useTheme(); + const styles = stylesFn(theme); + const [membersInContract, setMembersInContract] = useState([]); + const [addMemberModal, setAddMemberModal] = useState(false); + const [feedbackModalVisible, setFeedbackModalVisible] = useState(false); + const [updateMemberContainer, setUpdateMemberContainer] = useState(0); + + return ( + + {/* Collaboration Details */} + + {props?.applicationData?.attachments && + props?.applicationData?.attachments.length > 0 && ( + + processRawAttachment(attachment) + ) || [] + } + /> + )} + + {/* About Collaboration */} + + + + {props.userData.name} + + {props.collaborationDetail.timeStamp ? ( + + {formatDistanceToNow(props.collaborationDetail.timeStamp, { + addSuffix: true, + })} + + ) : null} + + + No content available.

", + }} + contentWidth={Dimensions.get("window").width} + defaultTextProps={{ + style: { + color: Colors(theme).text, + fontSize: 16, + lineHeight: 22, + }, + }} + /> +
+
+ + setFeedbackModalVisible(true)} + userData={props.userData} + /> + + setAddMemberModal(true)} + key={updateMemberContainer} + updateMemberContainer={updateMemberContainer} + /> + + {}} + /> + { + router.push( + `/collaboration-details/${props.contractData.collaborationId}` + ); + }} + > + + + {props.collaborationDetail.name} + + + + + {props.collaborationDetail.description} + + +
+
+ setAddMemberModal(false)} + visible={addMemberModal} + membersAlreadyInContract={membersInContract} + channelId={props.contractData.streamChannelId} + refreshData={props.refreshData} + updateMemberContainer={() => + setUpdateMemberContainer((prev) => prev + 1) + } + /> + {props.contractData.status === 2 && ( + + setFeedbackModalVisible(false)} + star={props.contractData.feedbackFromBrand?.ratings || 0} + visible={feedbackModalVisible} + contract={props.contractData} + refreshData={props.refreshData} + /> + + )} +
+ ); +}; + +export default ContractDetailsContent; diff --git a/components/contracts/FeedbackModal.tsx b/components/contracts/FeedbackModal.tsx new file mode 100644 index 00000000..79f7856a --- /dev/null +++ b/components/contracts/FeedbackModal.tsx @@ -0,0 +1,212 @@ +import React, { useState } from "react"; +import { Image, Pressable, Linking, StyleSheet, TextInput } from "react-native"; +import { useTheme } from "@react-navigation/native"; +import { Text, View } from "@/components/theme/Themed"; +import { Chip, Modal, Portal } from "react-native-paper"; +import Colors from "@/constants/Colors"; +import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"; +import { faClose, faCross, faStar } from "@fortawesome/free-solid-svg-icons"; +import { doc, updateDoc } from "firebase/firestore"; +import { FirestoreDB } from "@/utils/firestore"; +import { IContracts } from "@/shared-libs/firestore/trendly-pro/models/contracts"; +import { useAuthContext } from "@/contexts"; + +interface FeedbackModalProps { + star: number; + feedbackGiven: boolean; + visible: boolean; + setVisibility: (visible: boolean) => void; + contract: IContracts; + refreshData: () => void; +} + +const FeedbackModal: React.FC = ({ + star, + feedbackGiven, + visible, + setVisibility, + contract, + refreshData, +}) => { + const theme = useTheme(); + const [selectedStar, setSelectedStar] = useState(star); + const [textFeedback, setTextFeedback] = useState(""); + const { manager } = useAuthContext(); + + const provideFeedback = async () => { + try { + const contractRef = doc( + FirestoreDB, + "contracts", + contract.streamChannelId + ); + if (textFeedback === "" || selectedStar === 0) { + alert("Please provide feedback and rating before submitting"); + } + const date = new Date(); + await updateDoc(contractRef, { + feedbackFromBrand: { + ratings: selectedStar, + feedbackReview: textFeedback, + managerId: manager?.id, + timeSubmitted: date.getTime(), + }, + }); + + if (contract.feedbackFromInfluencer?.ratings) { + await updateDoc(contractRef, { + status: 3, + }); + } + setVisibility(false); + refreshData(); + } catch (e) { + console.log(e); + } + }; + + return ( + setVisibility(false)} + contentContainerStyle={{ + backgroundColor: Colors(theme).background, + borderRadius: 10, + padding: 20, + marginHorizontal: 20, + }} + > + + + Feedback + setVisibility(false)}> + + + + + + {feedbackGiven + ? "Thank you for your feedback!" + : "Please provide your feedback"} + + {star === 0 && ( + + {[1, 2, 3, 4, 5].map((i) => ( + setSelectedStar(i)}> + + + ))} + + )} + {!feedbackGiven && ( + setTextFeedback(text)} + numberOfLines={5} + multiline + /> + )} + {!feedbackGiven && ( + { + provideFeedback(); + }} + > + + Submit Feedback + + + )} + {feedbackGiven && ( + setVisibility(false)} + > + + Close + + + )} + + + + ); +}; + +const styles = StyleSheet.create({ + modal: { + width: "100%", + justifyContent: "center", + alignItems: "center", + }, + modalHeader: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + width: "100%", + }, + modalTitle: { + fontSize: 20, + fontWeight: "bold", + }, + modalContent: { + width: "100%", + alignItems: "center", + }, + modalText: { + fontSize: 16, + marginVertical: 10, + }, + modalRating: { + flexDirection: "row", + marginVertical: 10, + }, + textInput: { + width: "100%", + height: 100, + borderWidth: 1, + borderRadius: 5, + padding: 10, + fontSize: 16, + marginVertical: 10, + }, +}); + +export default FeedbackModal; diff --git a/components/contracts/MemberContainer.tsx b/components/contracts/MemberContainer.tsx new file mode 100644 index 00000000..655ab28b --- /dev/null +++ b/components/contracts/MemberContainer.tsx @@ -0,0 +1,107 @@ +import { useTheme } from "@react-navigation/native"; +import { Text, View } from "../theme/Themed"; +import { FC, useEffect } from "react"; +import { Button } from "react-native-paper"; +import Colors from "@/constants/Colors"; +import React from "react"; +import { faNoteSticky } from "@fortawesome/free-regular-svg-icons"; +import { useChatContext } from "@/contexts"; +import { doc, getDoc } from "firebase/firestore"; +import { FirestoreDB } from "@/utils/firestore"; +import { FlatList, Pressable } from "react-native"; +import MembersCard from "../brand-profile/members-card"; +import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; + +interface MemberContainerProps { + channelId: string; + setMembersFromBrand: (members: any[]) => void; + updateMemberContainer: number; + setShowModal: () => void; +} + +const MemberContainer: FC = ({ + channelId, + setMembersFromBrand, + updateMemberContainer, + setShowModal, +}) => { + const theme = useTheme(); + const { fetchMembers } = useChatContext(); + const [updateMember, setUpdateMember] = React.useState(false); + const [members, setMembers] = React.useState([]); + + const fetchMembersFromClient = async () => { + try { + const members = await fetchMembers(channelId); + const memberData = await Promise.all( + members.map(async (member: any) => { + const memberRef = doc(FirestoreDB, "managers", member.user.id); + const memberDoc = await getDoc(memberRef); + const memberData = { + ...memberDoc.data(), + email: memberDoc.data()?.email, + managerId: member.user.id, + }; + if (memberData && memberData.email) { + return memberData; + } else { + return null; + } + }) + ); + const validMembers = memberData.filter((data) => data !== null); + + // Set the filtered members + setMembers(validMembers); + + setMembersFromBrand(validMembers); + } catch (e) { + console.log(e); + } + }; + + useEffect(() => { + fetchMembersFromClient(); + }, [updateMemberContainer]); + + return ( + + + + Members + + + + + + } + contentContainerStyle={{ + gap: 10, + }} + /> + + ); +}; + +export default MemberContainer; diff --git a/components/contracts/active.tsx b/components/contracts/active.tsx new file mode 100644 index 00000000..23604bee --- /dev/null +++ b/components/contracts/active.tsx @@ -0,0 +1,236 @@ +import BottomSheetActions from "@/components/BottomSheetActions"; +import { Text, View } from "@/components/theme/Themed"; +import Colors from "@/constants/Colors"; +import AppLayout from "@/layouts/app-layout"; +import { useTheme } from "@react-navigation/native"; +import { router } from "expo-router"; +import { useEffect, useMemo, useState } from "react"; +import { + collection, + getDocs, + query, + where, + collectionGroup, + doc, + getDoc, +} from "firebase/firestore"; +import { ActivityIndicator, FlatList } from "react-native"; +import { FirestoreDB } from "@/utils/firestore"; +import { AuthApp } from "@/utils/auth"; +import { RefreshControl } from "react-native"; +import { stylesFn } from "@/styles/Proposal.styles"; +import { useBrandContext } from "@/contexts/brand-context.provider"; +import EmptyState from "../ui/empty-state"; +import { useBreakpoints } from "@/hooks"; +import { Card } from "react-native-paper"; +import { IContracts } from "@/shared-libs/firestore/trendly-pro/models/contracts"; +import { IUsers } from "@/shared-libs/firestore/trendly-pro/models/users"; +import { + IApplications, + ICollaboration, +} from "@/shared-libs/firestore/trendly-pro/models/collaborations"; +import ContractHeader from "../contract-card/ContractHeader"; +import ContractDetails from "../contract-card/ContractDetails"; + +interface ICollaborationCard extends IContracts { + userData: IUsers; + applications: IApplications[]; + collaborationData: ICollaboration; +} + +const ActiveContracts = () => { + const [isVisible, setIsVisible] = useState(false); + const [proposals, setProposals] = useState([]); + const [selectedCollabId, setSelectedCollabId] = useState(null); + const [notPendingProposals, setNotPendingProposals] = useState(); + const { selectedBrand } = useBrandContext(); + + const openBottomSheet = (id: string) => { + setIsVisible(true); + setSelectedCollabId(id); + }; + const closeBottomSheet = () => setIsVisible(false); + + const theme = useTheme(); + const styles = stylesFn(theme); + const user = AuthApp.currentUser; + + const [isLoading, setIsLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + + const { xl } = useBreakpoints(); + + const handleRefresh = async () => { + setRefreshing(true); + await fetchProposals(); + setRefreshing(false); + }; + + const fetchProposals = async () => { + try { + setIsLoading(true); + const user = AuthApp.currentUser; + + if (!user?.uid) { + throw new Error("User not authenticated"); + } + + const contractsCol = collection(FirestoreDB, "contracts"); + const contractsSnapshot = await getDocs(contractsCol); + + const contracts = await Promise.all( + contractsSnapshot.docs.map(async (document) => { + const contract = document.data() as IContracts; + const collaborationId = contract.collaborationId; + + const user = doc(FirestoreDB, "users", contract.userId); + const userSnapshot = await getDoc(user); + const userData = userSnapshot.data() as IUsers; + + const hasAppliedQuery = query( + collectionGroup(FirestoreDB, "applications"), + where("userId", "==", contract.userId), + where("collaborationId", "==", collaborationId) + ); + + const hasAppliedSnapshot = await getDocs(hasAppliedQuery); + + //@ts-ignore + const applications = hasAppliedSnapshot.docs.map((appDoc) => ({ + id: appDoc.id, + ...appDoc.data(), + })) as IApplications[]; + + const collaborationRef = doc( + FirestoreDB, + "collaborations", + collaborationId + ); + const collaborationSnapshot = await getDoc(collaborationRef); + const collaborationData = + collaborationSnapshot.data() as ICollaboration; + + return { ...contract, applications, userData, collaborationData }; + }) + ); + + setProposals(contracts); + setIsLoading(false); + } catch (error) { + console.error("Error fetching proposals: ", error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchProposals(); + }, [user, selectedBrand]); + + const filteredProposals = useMemo(() => { + return proposals.filter((proposal) => { + return ( + proposal.status !== 3 && + proposal.collaborationData.brandId === selectedBrand?.id + ); + }); + }, [proposals]); + + if (isLoading) { + return ( + + + + + + ); + } + + return ( + + {filteredProposals.length === 0 ? ( + router.push("/collaborations")} + actionLabel="Explore Collaborations" + /> + ) : ( + + ( + { + router.push(`/contract-details/${item.streamChannelId}`); + }} + > + {}} + /> + + + )} + keyExtractor={(item, index) => index.toString()} + style={{ + flexGrow: 1, + paddingBottom: 16, + paddingHorizontal: 16, + paddingTop: 8, + }} + contentContainerStyle={{ + gap: 16, + paddingBottom: 24, + alignItems: xl ? "center" : "stretch", + }} + refreshControl={ + + } + horizontal={false} + numColumns={xl ? 2 : 1} // TODO: On fly can't be responsive + {...(xl && { + columnWrapperStyle: { + gap: 16, + }, + })} + /> + + )} + {isVisible && ( + + )} + + ); +}; + +export default ActiveContracts; diff --git a/components/contracts/past.tsx b/components/contracts/past.tsx new file mode 100644 index 00000000..8520b249 --- /dev/null +++ b/components/contracts/past.tsx @@ -0,0 +1,236 @@ +import BottomSheetActions from "@/components/BottomSheetActions"; +import { View } from "@/components/theme/Themed"; +import Colors from "@/constants/Colors"; +import AppLayout from "@/layouts/app-layout"; +import { useTheme } from "@react-navigation/native"; +import { router } from "expo-router"; +import { useEffect, useMemo, useState } from "react"; +import { + collection, + getDocs, + query, + where, + collectionGroup, + doc, + getDoc, +} from "firebase/firestore"; +import { ActivityIndicator, FlatList } from "react-native"; +import { FirestoreDB } from "@/utils/firestore"; +import { AuthApp } from "@/utils/auth"; +import { RefreshControl } from "react-native"; +import { stylesFn } from "@/styles/Proposal.styles"; +import { useBrandContext } from "@/contexts/brand-context.provider"; +import EmptyState from "../ui/empty-state"; +import { useBreakpoints } from "@/hooks"; +import { Card } from "react-native-paper"; +import { IContracts } from "@/shared-libs/firestore/trendly-pro/models/contracts"; +import { IUsers } from "@/shared-libs/firestore/trendly-pro/models/users"; +import { + IApplications, + ICollaboration, +} from "@/shared-libs/firestore/trendly-pro/models/collaborations"; +import ContractHeader from "../contract-card/ContractHeader"; +import ContractDetails from "../contract-card/ContractDetails"; + +interface ICollaborationCard extends IContracts { + userData: IUsers; + applications: IApplications[]; + collaborationData: ICollaboration; +} + +const PastContracts = () => { + const [isVisible, setIsVisible] = useState(false); + const [proposals, setProposals] = useState([]); + const [selectedCollabId, setSelectedCollabId] = useState(null); + const [notPendingProposals, setNotPendingProposals] = useState(); + const { selectedBrand } = useBrandContext(); + + const openBottomSheet = (id: string) => { + setIsVisible(true); + setSelectedCollabId(id); + }; + const closeBottomSheet = () => setIsVisible(false); + + const theme = useTheme(); + const styles = stylesFn(theme); + const user = AuthApp.currentUser; + + const [isLoading, setIsLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + + const { xl } = useBreakpoints(); + + const handleRefresh = async () => { + setRefreshing(true); + await fetchProposals(); + setRefreshing(false); + }; + + const fetchProposals = async () => { + try { + setIsLoading(true); + const user = AuthApp.currentUser; + + if (!user?.uid) { + throw new Error("User not authenticated"); + } + + const contractsCol = collection(FirestoreDB, "contracts"); + const contractsSnapshot = await getDocs(contractsCol); + + const contracts = await Promise.all( + contractsSnapshot.docs.map(async (document) => { + const contract = document.data() as IContracts; + const collaborationId = contract.collaborationId; + + const user = doc(FirestoreDB, "users", contract.userId); + const userSnapshot = await getDoc(user); + const userData = userSnapshot.data() as IUsers; + + const hasAppliedQuery = query( + collectionGroup(FirestoreDB, "applications"), + where("userId", "==", contract.userId), + where("collaborationId", "==", collaborationId) + ); + + const hasAppliedSnapshot = await getDocs(hasAppliedQuery); + + //@ts-ignore + const applications = hasAppliedSnapshot.docs.map((appDoc) => ({ + id: appDoc.id, + ...appDoc.data(), + })) as IApplications[]; + + const collaborationRef = doc( + FirestoreDB, + "collaborations", + collaborationId + ); + const collaborationSnapshot = await getDoc(collaborationRef); + const collaborationData = + collaborationSnapshot.data() as ICollaboration; + + return { ...contract, applications, userData, collaborationData }; + }) + ); + + setProposals(contracts); + setIsLoading(false); + } catch (error) { + console.error("Error fetching proposals: ", error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchProposals(); + }, [user, selectedBrand]); + + const filteredProposals = useMemo(() => { + return proposals.filter((proposal) => { + return ( + proposal.status === 3 && + proposal.collaborationData.brandId === selectedBrand?.id + ); + }); + }, [proposals]); + + if (isLoading) { + return ( + + + + + + ); + } + + return ( + + {filteredProposals.length === 0 ? ( + router.push("/collaborations")} + actionLabel="Explore Collaborations" + /> + ) : ( + + ( + { + router.push(`/contract-details/${item.streamChannelId}`); + }} + > + {}} + /> + + + )} + keyExtractor={(item, index) => index.toString()} + style={{ + flexGrow: 1, + paddingBottom: 16, + paddingHorizontal: 16, + paddingTop: 8, + }} + contentContainerStyle={{ + gap: 16, + paddingBottom: 24, + alignItems: xl ? "center" : "stretch", + }} + refreshControl={ + + } + horizontal={false} + numColumns={xl ? 2 : 1} // TODO: On fly can't be responsive + {...(xl && { + columnWrapperStyle: { + gap: 16, + }, + })} + /> + + )} + {isVisible && ( + + )} + + ); +}; + +export default PastContracts; diff --git a/components/menu/index.tsx b/components/menu/index.tsx index f376eb81..a1a10213 100644 --- a/components/menu/index.tsx +++ b/components/menu/index.tsx @@ -5,7 +5,7 @@ import stylesFn from "@/styles/menu/MenuItem.styles"; import { useTheme } from "@react-navigation/native"; import { useRouter } from "expo-router"; import { MENU_ITEMS } from "@/constants/Menu"; -import { Image, ImageBackground, Pressable } from "react-native"; +import { Image, ImageBackground, Pressable, ScrollView } from "react-native"; import { useBrandContext } from "@/contexts/brand-context.provider"; import { useState } from "react"; import ConfirmationModal from "../ui/modal/ConfirmationModal"; @@ -31,7 +31,13 @@ const Menu = () => { }; return ( - + { /> {selectedBrand?.name} {selectedBrand?.profile?.about && ( - + {selectedBrand?.profile?.about} )} @@ -129,7 +139,7 @@ const Menu = () => { setVisible={setLogoutModalVisible} visible={logoutModalVisible} /> - + ); }; diff --git a/components/preferences/index.tsx b/components/preferences/index.tsx index b4781823..efb7ba81 100644 --- a/components/preferences/index.tsx +++ b/components/preferences/index.tsx @@ -1,8 +1,109 @@ -import { View } from "../theme/Themed" +import { FlatList, ScrollView } from "react-native"; +import MembersCard from "../brand-profile/members-card"; +import { View } from "../theme/Themed"; +import Colors from "@/constants/Colors"; +import { useTheme } from "@react-navigation/native"; +import { useBrandContext } from "@/contexts/brand-context.provider"; +import { collection, doc, getDoc, getDocs } from "firebase/firestore"; +import { FirestoreDB } from "@/utils/firestore"; +import { IBrandsMembers } from "@/shared-libs/firestore/trendly-pro/models/brands"; +import { useEffect, useState } from "react"; +import { IManagers } from "@/shared-libs/firestore/trendly-pro/models/managers"; +import AppLayout from "@/layouts/app-layout"; +import { Button, Portal } from "react-native-paper"; +import MembersModal from "../ui/modal/MembersModal"; + +export interface ManagerCard extends IManagers { + managerId: string; + status: number; +} const Preferences = () => { + const theme = useTheme(); + const { selectedBrand } = useBrandContext(); + const [members, setMembers] = useState([]); + const [showMemberModal, setShowMemberModal] = useState(false); + const fetchMembers = async () => { + if (!selectedBrand) return; + try { + const memberRef = collection( + FirestoreDB, + "brands", + selectedBrand.id, + "members" + ); + const memberDoc = await getDocs(memberRef); + const membersData = memberDoc.docs.map((doc) => { + return { + ...doc.data(), + managerId: doc.id, + } as ManagerCard; + }); + + const members = await Promise.all( + membersData.map(async (member) => { + const memberDoc = doc(FirestoreDB, "managers", member.managerId); + const memberData = getDoc(memberDoc).then((doc) => { + return { + ...doc.data(), + managerId: doc.id, + status: member.status, + } as ManagerCard; + }); + return memberData; + }) + ); + + setMembers(members); + } catch (error) { + console.error(error); + } + }; + + useEffect(() => { + fetchMembers(); + }, [selectedBrand]); + return ( - + + {/* */} + } + keyExtractor={(item) => item.managerId} + contentContainerStyle={{ + gap: 10, + }} + /> + + + { + setShowMemberModal(false); + }} + refresh={fetchMembers} + theme={theme} + /> + + ); }; diff --git a/components/profile/index.tsx b/components/profile/index.tsx index 351f79e2..0505786e 100644 --- a/components/profile/index.tsx +++ b/components/profile/index.tsx @@ -21,6 +21,13 @@ import { FirestoreDB } from "@/utils/firestore"; import Toaster from "@/shared-uis/components/toaster/Toaster"; import ScreenHeader from "../ui/screen-header"; import { Text } from "../theme/Themed"; +import { + ref, + uploadString, + getDownloadURL, + uploadBytes, +} from "firebase/storage"; +import { StorageApp } from "@/utils/firebase-storage"; const Profile = () => { const [name, setName] = useState(""); @@ -28,46 +35,46 @@ const Profile = () => { const [email, setEmail] = useState(""); const [isModalVisible, setIsModalVisible] = useState(false); const [loading, setLoading] = useState(false); // Loader state - const { uploadImageBytes } = useFirebaseStorageContext(); const { manager } = useAuthContext(); const [capturedImage, setCapturedImage] = useState( manager?.profileImage || "" ); + const [imageToUpload, setImageToUpload] = useState(""); const theme = useTheme(); - console.log(capturedImage); - - const updateProfile = async (image: string) => { + const updateProfile = async () => { if (!manager || !manager.id) { console.error("Manager ID is missing"); return; } - if (!capturedImage) { - console.error("Captured image is invalid or empty"); - return; - } - const managerRef = doc(FirestoreDB, "managers", manager.id); setLoading(true); try { - const path = `managers/${manager.id}/profile-image`; - if (capturedImage !== manager.profileImage) { - const blob = await fetch(capturedImage).then((r) => r.blob()); - const url = await uploadImageBytes(blob, path); - await updateDoc(managerRef, { - profileImage: url, - }); - } + let updatedImageURL = manager.profileImage; // Default to existing profile image + + if (imageToUpload) { + const path = `managers/${manager.id}/profile-image`; + + // Validate Base64 format - if (name !== manager.name) { - await updateDoc(managerRef, { - name, - }); + const storageRef = ref(StorageApp, path); + const blobImage = await fetch(imageToUpload).then((res) => res.blob()); + await uploadBytes(storageRef, blobImage); + + // // Get the downloadable URL for the uploaded image + updatedImageURL = await getDownloadURL(storageRef); } + // Update Firestore document + await updateDoc(managerRef, { + profileImage: updatedImageURL, + name: name || manager.name, + }); + + setImageToUpload(""); // Clear image-to-upload buffer Toaster.success("Profile updated successfully"); } catch (error) { console.error("Error during profile update:", error); @@ -97,7 +104,7 @@ const Profile = () => { title="Profile" rightAction rightActionButton={ - updateProfile(capturedImage)}> + { diff --git a/components/ui/modal/MembersModal.tsx b/components/ui/modal/MembersModal.tsx new file mode 100644 index 00000000..61081ea3 --- /dev/null +++ b/components/ui/modal/MembersModal.tsx @@ -0,0 +1,94 @@ +import React from "react"; +import { Button, Chip, Modal, TextInput } from "react-native-paper"; +import { stylesFn } from "@/styles/Members"; +import { useTheme } from "@react-navigation/native"; +import { View, Text } from "@/components/theme/Themed"; +import Colors from "@/constants/Colors"; +import { useBrandContext } from "@/contexts/brand-context.provider"; +import Toaster from "@/shared-uis/components/toaster/Toaster"; +import axios from "axios"; +import { useAuthContext } from "@/contexts"; +import { AuthApp } from "@/utils/auth"; + +interface MembersModalProps { + visible: boolean; + handleModalClose: () => void; + theme: any; + refresh: () => void; +} + +const MembersModal: React.FC = ({ + visible, + handleModalClose, + theme, + refresh, +}) => { + const styles = stylesFn(theme); + + const [email, setEmail] = React.useState(""); + const { selectedBrand } = useBrandContext(); + + const addMember = async () => { + if (!selectedBrand) return; + if (!email) { + Toaster.error("Please enter email"); + } + const user = await AuthApp.currentUser?.getIdToken(); + await axios + .post( + "https://be.trendly.pro/api/v1/brands/members", + { + brandId: selectedBrand.id, + email, + }, + { + headers: { + Authorization: `Bearer ${user}`, + }, + } + ) + .then((res) => { + Toaster.success("User Invited Successfully"); + refresh(); + handleModalClose(); + }) + .catch((e) => { + Toaster.error("Something wrong happened"); + console.error(e); + }); + }; + + return ( + { + handleModalClose(); + }} + style={{ + justifyContent: "center", + alignItems: "center", + }} + > + + Add Member + You can add members from here + + setEmail(value)} + style={styles.input} + autoFocus + /> + + + + + + ); +}; + +export default MembersModal; diff --git a/components/view-collaboration/ViewCollaborationMap.tsx b/components/view-collaboration/ViewCollaborationMap.tsx new file mode 100644 index 00000000..9551c8f8 --- /dev/null +++ b/components/view-collaboration/ViewCollaborationMap.tsx @@ -0,0 +1,112 @@ +import { useEffect, useState } from "react"; +import { Platform } from "react-native"; +import MapView, { Marker } from "react-native-maps"; +import { APIProvider } from "@vis.gl/react-google-maps"; + +import Map from "@/components/map"; +import { View } from "@/components/theme/Themed"; +import stylesFn from "@/styles/modal/UploadModal.styles"; +import { useTheme } from "@react-navigation/native"; + +interface CreateCollaborationMapProps { + mapRegion: any; + onMapRegionChange: (region: any) => void; + onFormattedAddressChange?: (address: string) => void; +} + +const ViewCollaborationMap: React.FC = ({ + mapRegion, + onMapRegionChange, + onFormattedAddressChange, +}) => { + const [nativeMapRegion, setNativeMapRegion] = useState(mapRegion); + + const [webMapRegion, setWebMapRegion] = useState(mapRegion); + + const theme = useTheme(); + const styles = stylesFn(theme); + + const fetchMapRegionAddress = async (lat: number, lng: number) => { + if (!lat || !lng) { + return; + } + + const response = await fetch( + `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&key=${process.env.EXPO_PUBLIC_GOOGLE_MAPS_API_KEY}` + ); + + const result = await response.json(); + + const address = + result?.results?.[0]?.formatted_address || + result?.plus_code?.compound_code || + "Address"; + + return address; + }; + + const onLocationChange = async (location: { + latitude: number; + longitude: number; + }) => { + setWebMapRegion({ + latitude: location.latitude, + longitude: location.longitude, + }); + + const address = await fetchMapRegionAddress( + location.latitude, + location.longitude + ); + + if (onFormattedAddressChange) { + onFormattedAddressChange(address as string); + } + }; + + const onMapLocationChange = async (region: any) => { + onMapRegionChange({ + latitude: region.latitude, + longitude: region.longitude, + }); + + const address = await fetchMapRegionAddress( + region.latitude, + region.longitude + ); + + if (onFormattedAddressChange) { + onFormattedAddressChange(address as string); + } + }; + + useEffect(() => { + if (Platform.OS === "web") { + onMapLocationChange(webMapRegion); + } else { + onMapLocationChange(nativeMapRegion); + } + }, [nativeMapRegion, webMapRegion]); + + if (Platform.OS === "web") { + return ( + + {}} /> + + ); + } + + return ( + + {}} + > + + + + ); +}; + +export default ViewCollaborationMap; diff --git a/constants/Colors.ts b/constants/Colors.ts index a2e0ad2c..3c6b19ce 100644 --- a/constants/Colors.ts +++ b/constants/Colors.ts @@ -42,8 +42,12 @@ export default (theme: Theme) => ({ whiteSmoke: "#f5f5f5", success: "#d4edda", successForeground: "#28a745", + gray100: "#555", + gray200: "#f9f9f9", + gray300: "#757575", pink: "#f8d7da", pinkForeground: "#dc3545", + gold: "#ECD694", backdrop: "rgba(0, 0, 0, 0.5)", black: "#000", lightgray: "lightgray", @@ -52,11 +56,11 @@ export default (theme: Theme) => ({ platinum: "#DBDBDB", red: "red", notificationDot: "red", - gray100: "#555", onPrimary: "#ffffff", secondary: "#054463", secondaryContainer: "#538BA6", onSecondaryContainer: "#ffffff", + green: "#9DD586", surface: "#ffffff", surfaceVariant: "#ffffff", transparent: "transparent", diff --git a/contexts/chat-context.provider.native.tsx b/contexts/chat-context.provider.native.tsx index 54fec79d..02639ae9 100644 --- a/contexts/chat-context.provider.native.tsx +++ b/contexts/chat-context.provider.native.tsx @@ -11,19 +11,29 @@ import { useAuthContext } from "./auth-context.provider"; import { useTheme } from "@react-navigation/native"; import { useStreamTheme } from "@/hooks"; -const streamClient = StreamChat.getInstance(process.env.EXPO_PUBLIC_STREAM_API_KEY!); +const streamClient = StreamChat.getInstance( + process.env.EXPO_PUBLIC_STREAM_API_KEY! +); interface ChatContextProps { createGroupWithMembers: ( groupName: string, - members: string[], + members: string[] ) => Promise; connectUser: () => void; + fetchMembers: (channel: string) => Promise; + addMemberToChannel: (channel: string, member: string) => void; + sendSystemMessage: (channel: string, message: string) => void; + fetchChannelCid: (channelId: string) => Promise; } const ChatContext = createContext({ createGroupWithMembers: async () => Promise.resolve({} as Channel), - connectUser: async () => { }, + connectUser: async () => {}, + fetchMembers: async () => {}, + addMemberToChannel: async () => {}, + sendSystemMessage: async () => {}, + fetchChannelCid: async () => "", }); export const useChatContext = () => useContext(ChatContext); @@ -33,51 +43,49 @@ export const ChatContextProvider: React.FC = ({ }) => { const [isReady, setIsReady] = useState(false); const theme = useTheme(); - const { - getTheme, - } = useStreamTheme(theme); + const { getTheme } = useStreamTheme(theme); const [streamChatTheme, setStreamChatTheme] = useState(getTheme()); - const [client, setClient] = useState | null>(null); + const [client, setClient] = useState | null>( + null + ); useEffect(() => { setStreamChatTheme(getTheme()); }, [theme]); - const { - manager: user, - } = useAuthContext(); - - const connect = async ( - streamToken: string, - ) => { - await streamClient.connectUser( - { - id: user?.id as string, - name: user?.name as string, - image: user?.profileImage as string || '', - }, - streamToken, - ).then(() => { - setClient(streamClient); - setIsReady(true); - }); - } + const { manager: user } = useAuthContext(); + + const connect = async (streamToken: string) => { + await streamClient + .connectUser( + { + id: user?.id as string, + name: user?.name as string, + image: (user?.profileImage as string) || "", + }, + streamToken + ) + .then(() => { + setClient(streamClient); + setIsReady(true); + }); + }; const connectUser = async () => { - const response = await fetch('https://be.trendly.pro/api/v1/chat/connect', { - method: 'POST', + const response = await fetch("https://be.trendly.pro/api/v1/chat/connect", { + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${user?.id}`, + "Content-Type": "application/json", + Authorization: `Bearer ${user?.id}`, }, }); const data = await response.json(); - if (data.token !== '') { + if (data.token !== "") { await connect(data.token); } - } + }; useEffect(() => { if (user?.id) { @@ -94,13 +102,13 @@ export const ChatContextProvider: React.FC = ({ const createGroupWithMembers = async ( groupName: string, - members: string[], + members: string[] ): Promise => { - const response = await fetch('https://be.trendly.pro/api/v1/chat/channel', { - method: 'POST', + const response = await fetch("https://be.trendly.pro/api/v1/chat/channel", { + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${user?.id}`, + "Content-Type": "application/json", + Authorization: `Bearer ${user?.id}`, }, body: JSON.stringify({ name: groupName, @@ -113,6 +121,47 @@ export const ChatContextProvider: React.FC = ({ return data.channel; }; + const fetchMembers = async (channel: string) => { + const channelToWatch = streamClient.channel("messaging", channel); + channelToWatch.watch(); + const membersList = Object.values(channelToWatch.state.members); + + return membersList; + }; + + const addMemberToChannel = async (channel: string, member: string) => { + try { + const channelToWatch = streamClient.channel("messaging", channel); + await channelToWatch + .addMembers([member]) + .then(() => {}) + .catch((error) => { + console.error(error); + }); + } catch (error) { + console.error(error); + } + }; + + const sendSystemMessage = async (channel: string, message: string) => { + const channelToWatch = streamClient.channel("messaging", channel); + const messageToSend = { + text: message, + user: { + id: "system", + name: "system", + }, + type: "system", + }; + channelToWatch.sendMessage(messageToSend); + }; + + const fetchChannelCid = async (channelId: string) => { + const channel = streamClient.channel("messaging", channelId); + await channel.watch(); + return channel.cid; + }; + return ( @@ -120,6 +169,10 @@ export const ChatContextProvider: React.FC = ({ value={{ createGroupWithMembers, connectUser, + fetchMembers, + addMemberToChannel, + sendSystemMessage, + fetchChannelCid, }} > {children} diff --git a/contexts/chat-context.provider.web.tsx b/contexts/chat-context.provider.web.tsx index 3de649e6..7eee2b91 100644 --- a/contexts/chat-context.provider.web.tsx +++ b/contexts/chat-context.provider.web.tsx @@ -4,14 +4,22 @@ import { useAuthContext } from "./auth-context.provider"; interface ChatContextProps { createGroupWithMembers: ( groupName: string, - members: string[], + members: string[] ) => Promise; connectUser: () => void; + fetchMembers: (channel: any) => Promise; + addMemberToChannel: (channel: any, member: string) => void; + sendSystemMessage: (channel: string, message: string) => void; + fetchChannelCid: (channelId: string) => Promise; } const ChatContext = createContext({ - createGroupWithMembers: async () => { }, - connectUser: async () => { }, + createGroupWithMembers: async () => {}, + connectUser: async () => {}, + fetchMembers: async () => {}, + addMemberToChannel: async () => {}, + sendSystemMessage: async () => {}, + fetchChannelCid: async () => "", }); export const useChatContext = () => useContext(ChatContext); @@ -19,19 +27,17 @@ export const useChatContext = () => useContext(ChatContext); export const ChatContextProvider: React.FC = ({ children, }) => { - const { - manager: user, - } = useAuthContext(); + const { manager: user } = useAuthContext(); const createGroupWithMembers = async ( groupName: string, - members: string[], + members: string[] ): Promise => { - const response = await fetch('https://be.trendly.pro/api/v1/chat/channel', { - method: 'POST', + const response = await fetch("https://be.trendly.pro/api/v1/chat/channel", { + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${user?.id}`, + "Content-Type": "application/json", + Authorization: `Bearer ${user?.id}`, }, body: JSON.stringify({ name: groupName, @@ -44,6 +50,16 @@ export const ChatContextProvider: React.FC = ({ return data.channel; }; + const fetchMembers = async (channel: any): Promise => {}; + + const addMemberToChannel = async (channel: any, member: string) => {}; + + const sendSystemMessage = async (channel: string, message: string) => {}; + + const fetchChannelCid = async (channelId: string): Promise => { + return ""; + }; + const connectUser = async () => { return null; }; @@ -53,6 +69,10 @@ export const ChatContextProvider: React.FC = ({ value={{ createGroupWithMembers, connectUser, + fetchMembers, + addMemberToChannel, + sendSystemMessage, + fetchChannelCid, }} > {children} diff --git a/package-lock.json b/package-lock.json index 0ed4e93c..6401079b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "@react-navigation/native": "^6.0.2", "@stream-io/flat-list-mvcp": "^0.10.3", "@vis.gl/react-google-maps": "^1.4.0", + "axios": "^1.7.9", "date-fns": "^4.1.0", "expo": "~51.0.21", "expo-av": "^14.0.7", @@ -11886,9 +11887,10 @@ } }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", diff --git a/package.json b/package.json index 752fdbbd..b0a7a12b 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@react-navigation/native": "^6.0.2", "@stream-io/flat-list-mvcp": "^0.10.3", "@vis.gl/react-google-maps": "^1.4.0", + "axios": "^1.7.9", "date-fns": "^4.1.0", "expo": "~51.0.21", "expo-av": "^14.0.7", diff --git a/styles/CollaborationDetails.styles.tsx b/styles/CollaborationDetails.styles.tsx index b1c109bb..d1166f3a 100644 --- a/styles/CollaborationDetails.styles.tsx +++ b/styles/CollaborationDetails.styles.tsx @@ -22,13 +22,14 @@ export const stylesFn = (theme: Theme) => profileContent: { alignItems: "center", width: "100%", + gap: 16, }, name: { fontWeight: "bold", fontSize: 20, color: Colors(theme).text, lineHeight: 20, - marginTop: 8, + marginTop: 16, }, brandName: { fontSize: 16, @@ -38,7 +39,7 @@ export const stylesFn = (theme: Theme) => fontSize: 14, color: Colors(theme).text, textAlign: "left", - marginVertical: 8, + marginTop: 8, }, statsContainer: { flexDirection: "row", diff --git a/styles/Members.tsx b/styles/Members.tsx new file mode 100644 index 00000000..28f5a722 --- /dev/null +++ b/styles/Members.tsx @@ -0,0 +1,71 @@ +import Colors from "@/constants/Colors"; + +import { Theme } from "@react-navigation/native"; +import { StyleSheet, Dimensions } from "react-native"; + +export const stylesFn = (theme: Theme) => + StyleSheet.create({ + container: { + flex: 1, + padding: 16, + backgroundColor: Colors(theme).background, + }, + searchInput: { + backgroundColor: Colors(theme).background, + flex: 1, + }, + title: { + fontSize: 18, + fontWeight: "bold", + }, + subtitle: { + fontSize: 16, + }, + scrollView: { + paddingBottom: 160, + width: "100%", + }, + scrollViewContent: { + paddingBottom: 16, + }, + actionsCell: { + flexDirection: "row", + justifyContent: "flex-end", + }, + modalContent: { + padding: 20, + borderRadius: 10, + backgroundColor: Colors(theme).background, + width: 300, + gap: 12, + }, + modalInputContainer: { + marginBottom: 10, + backgroundColor: Colors(theme).background, + }, + input: { + marginBottom: 10, + backgroundColor: Colors(theme).background, + }, + chipContainer: { + backgroundColor: Colors(theme).background, + flexDirection: "row", + flexWrap: "wrap", + marginVertical: 10, + }, + chip: { + margin: 4, + }, + addButton: { + marginTop: 10, + }, + noDataContainer: { + flex: 1, + justifyContent: "center", + marginTop: 16, + alignItems: "center", + }, + noDataText: { + fontSize: 17, + }, + }); diff --git a/styles/menu/MenuItem.styles.tsx b/styles/menu/MenuItem.styles.tsx index 18b5efd5..5880309c 100644 --- a/styles/menu/MenuItem.styles.tsx +++ b/styles/menu/MenuItem.styles.tsx @@ -28,6 +28,7 @@ const stylesFn = (theme: Theme) => flex: 1, paddingHorizontal: 20, paddingVertical: 10, + justifyContent: "space-between", }, topRow: { gap: 20, diff --git a/styles/profile/ProfileItemCard.styles.tsx b/styles/profile/ProfileItemCard.styles.tsx index bdabc1a0..7f6c5e34 100644 --- a/styles/profile/ProfileItemCard.styles.tsx +++ b/styles/profile/ProfileItemCard.styles.tsx @@ -8,7 +8,6 @@ const styles = (theme: Theme) => flexDirection: "row", alignItems: "center", paddingVertical: 30, - paddingHorizontal: 20, borderBottomWidth: 0.2, gap: 12, }, @@ -29,4 +28,4 @@ const styles = (theme: Theme) => }, }); -export default styles; \ No newline at end of file +export default styles; From d31e768e7153c81a56487dfdd801fd4bd19fcd58 Mon Sep 17 00:00:00 2001 From: Manan Sharma <112090668+Manan-Sharma-5@users.noreply.github.com> Date: Thu, 2 Jan 2025 22:04:23 +0530 Subject: [PATCH 02/17] fixed member container --- contexts/chat-context.provider.native.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contexts/chat-context.provider.native.tsx b/contexts/chat-context.provider.native.tsx index 02639ae9..ae733bb4 100644 --- a/contexts/chat-context.provider.native.tsx +++ b/contexts/chat-context.provider.native.tsx @@ -123,7 +123,7 @@ export const ChatContextProvider: React.FC = ({ const fetchMembers = async (channel: string) => { const channelToWatch = streamClient.channel("messaging", channel); - channelToWatch.watch(); + await channelToWatch.watch(); const membersList = Object.values(channelToWatch.state.members); return membersList; From 3094cb185c6d05607d6ffa30d7766b5cd56d3498 Mon Sep 17 00:00:00 2001 From: Devender Singh Rathore <59788555+dev-rathore@users.noreply.github.com> Date: Thu, 2 Jan 2025 23:06:18 +0530 Subject: [PATCH 03/17] Redesign application and invitation cards (#51) * redesign application and invitation cards --- app/(main)/(drawer)/(tabs)/_layout.tsx | 41 ++-- .../(tabs)}/contracts.tsx | 0 .../(drawer)/(tabs)/create-collaboration.tsx | 11 - app/(main)/(screens)/_layout.tsx | 1 - assets/icons/verified.png | Bin 0 -> 3505 bytes .../application-card.tsx | 83 +++++++ .../collaboration-details/invitation-card.tsx | 88 ++++++++ .../card/profile-modal/application-card.tsx | 98 ++++++++ .../card/profile-modal/invitation-card.tsx | 57 +++++ .../ApplicationsTabContent.tsx | 211 ++++++++++++------ .../InvitationsTabContent.tsx | 192 +++++++++++----- .../collaboration-details/index.tsx | 1 + components/collaborations/index.tsx | 32 +++ .../drawer-layout/DrawerMenuContent.tsx | 60 ++--- components/ui/card/secondary/card-actions.tsx | 78 +++++++ .../ui/card/secondary/card-description.tsx | 27 +++ components/ui/card/secondary/card-footer.tsx | 37 +++ components/ui/card/secondary/card-header.tsx | 132 +++++++++++ components/ui/card/secondary/index.tsx | 30 +++ components/ui/card/tertiary/card-files.tsx | 62 +++++ components/ui/card/tertiary/card-footer.tsx | 21 ++ components/ui/card/tertiary/card-header.tsx | 41 ++++ components/ui/card/tertiary/card-metadata.tsx | 43 ++++ .../ui/card/tertiary/card-questions.tsx | 49 ++++ components/ui/card/tertiary/index.tsx | 32 +++ constants/Unit.ts | 2 + contexts/collaboration-context.provider.tsx | 1 + hooks/request/index.ts | 3 +- hooks/request/use-applications.tsx | 155 +++++++++++++ hooks/request/use-influencers.tsx | 6 +- hooks/use-invitation.ts | 27 +++ shared-libs | 2 +- shared-uis | 2 +- types/Collaboration.ts | 20 +- 34 files changed, 1456 insertions(+), 189 deletions(-) rename app/(main)/{(screens) => (drawer)/(tabs)}/contracts.tsx (100%) delete mode 100644 app/(main)/(drawer)/(tabs)/create-collaboration.tsx create mode 100644 assets/icons/verified.png create mode 100644 components/card/collaboration-details/application-card.tsx create mode 100644 components/card/collaboration-details/invitation-card.tsx create mode 100644 components/card/profile-modal/application-card.tsx create mode 100644 components/card/profile-modal/invitation-card.tsx create mode 100644 components/ui/card/secondary/card-actions.tsx create mode 100644 components/ui/card/secondary/card-description.tsx create mode 100644 components/ui/card/secondary/card-footer.tsx create mode 100644 components/ui/card/secondary/card-header.tsx create mode 100644 components/ui/card/secondary/index.tsx create mode 100644 components/ui/card/tertiary/card-files.tsx create mode 100644 components/ui/card/tertiary/card-footer.tsx create mode 100644 components/ui/card/tertiary/card-header.tsx create mode 100644 components/ui/card/tertiary/card-metadata.tsx create mode 100644 components/ui/card/tertiary/card-questions.tsx create mode 100644 components/ui/card/tertiary/index.tsx create mode 100644 constants/Unit.ts create mode 100644 hooks/request/use-applications.tsx create mode 100644 hooks/use-invitation.ts diff --git a/app/(main)/(drawer)/(tabs)/_layout.tsx b/app/(main)/(drawer)/(tabs)/_layout.tsx index 3f60454c..21d817ed 100644 --- a/app/(main)/(drawer)/(tabs)/_layout.tsx +++ b/app/(main)/(drawer)/(tabs)/_layout.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { router, Tabs } from "expo-router"; +import { Tabs } from "expo-router"; import Colors from "@/constants/Colors"; import { useClientOnlyValue } from "@/components/useClientOnlyValue"; @@ -10,12 +10,13 @@ import { View } from "@/components/theme/Themed"; import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"; import { faComment, + faFileLines, faStar, } from "@fortawesome/free-regular-svg-icons"; import { faComment as faCommentSolid, faHouseUser as faHouseUserSolid, - faPlusCircle, + faFileLines as faFileLinesSolid, faStar as faStarSolid, } from "@fortawesome/free-solid-svg-icons"; import Header from "@/components/explore-influencers/header"; @@ -48,7 +49,7 @@ const TabLayout = () => { ( focused ? ( { }} /> ( ), + title: "Messages", + headerTitleAlign: "left", headerRight: () => , }} /> ({ - tabPress: (e) => { - e.preventDefault(); - router.push({ - pathname: "/(modal)/create-collaboration", - }); - }, - })} + name="collaborations" options={{ - headerShown: false, - title: "Create Collaborations", - tabBarIcon: ({ color }) => ( + title: "Collaborations", + tabBarIcon: ({ color, focused }) => ( ), + headerRight: () => , }} /> ( ), - title: "Messages", - headerTitleAlign: "left", headerRight: () => , }} /> , }} /> diff --git a/app/(main)/(screens)/contracts.tsx b/app/(main)/(drawer)/(tabs)/contracts.tsx similarity index 100% rename from app/(main)/(screens)/contracts.tsx rename to app/(main)/(drawer)/(tabs)/contracts.tsx diff --git a/app/(main)/(drawer)/(tabs)/create-collaboration.tsx b/app/(main)/(drawer)/(tabs)/create-collaboration.tsx deleted file mode 100644 index 3ed3d7d2..00000000 --- a/app/(main)/(drawer)/(tabs)/create-collaboration.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { useRouter } from "expo-router"; - -const CreateCollaborationScreen = () => { - const router = useRouter(); - - router.push("/(modal)/create-collaboration"); - - return null; -}; - -export default CreateCollaborationScreen; diff --git a/app/(main)/(screens)/_layout.tsx b/app/(main)/(screens)/_layout.tsx index 02a5e13a..1ca52b16 100644 --- a/app/(main)/(screens)/_layout.tsx +++ b/app/(main)/(screens)/_layout.tsx @@ -11,7 +11,6 @@ const ScreensLayout = () => { }} > - diff --git a/assets/icons/verified.png b/assets/icons/verified.png new file mode 100644 index 0000000000000000000000000000000000000000..8719bd9ab861837e99d63497ceb246ada7cff095 GIT binary patch literal 3505 zcmaJ^XEYlQ`wdbAEfLhL6*IMIL#SFMvDMzup-n?YYE^AgI~A)&YHy{e(OM~L&l;`T zQq-tXKdb!s_xVtIUJxZ41z`i4M0EXs6iPISY|IQCq{hDZ2^?m5x zGIm~=Ah7d%wRPuc-mhJO3giHDLpi{bu|QzF2n|hv!EK5pJDdD}w~TqW(P87vY#F*4 z=V+s|l(4*aW?sO5Ho4wtaCD}LC#Grfh*KX)(o|)MEp6;urAS$5B~h591X_3e*&Bb2 z;#`Gj-6s<1SD-i^`iJRa@0L)U_$M43OiU2~-sN+M8n4UpBU8qu+LT({lv-oE?cs%Z znVNGes#|X|FYl;FvAYG#mD%QS(`)kzMBw9wWZ-j99^OK8dBv9Y4^QiNc4^NL9sTq- z4Q%)L!q_Mtkp3htr#~ux@V5g8I}T>tjtgy!We&zGG0#sUw&gu%$B0w~&z)mlmvEX))uUHYeUBdcPQ3GgGMGw>2}w6rjXKr9~gx{fx(NsbLqFLb8t4%Sl^e zS%6^sw;dj_&_9duzk%Bs1MezLx+B@A&jVb(bcD%5E->}G0qr_#TQ~}-OmWqiMXSb- z`$a@GQ279g8$sZ(81dvjEB6%zcZl?CM+!R~M3e;-K^<^r=d#qFgzM1w zUZv34RhnESEhv7iEnf9zv;ln;Mt*m6MY=e>C|gG`dN(1op)_ViP8TOxJXW552YZT^ z>HWeaT!d+Ir)3N4=zW6l!#k_1(0eXiEXUr$+TV?R{2*2icZb-Tz3^GWbno6?pDy&(XahAn74q$Mm2 zFmRg$xiE0G*4OKOo}Y@8>z=+!>nm8_dyJ9fVBu9feDCdo#m$4*DoV?GjMf*m0R?2~ zf-+(b3=t}d&R@*UI={~C!uT`hs`XKUnFd*5kGu}V2Ji>ZgQCay&u1X{m10axFQ4CzdcS>D1GGcU6g|unU(Uy&-7CV2gC(ZL*qyXW0_P|or$;C9 z%=s+JtrJ~-8^)EVgxE5w_8Yb(5X}q`>eXYy+(VLnn~tv=>lo%%aBzLCZPh4|W@u|w z{Zagr7!Ci9FSqT#ZtSh3v~pbh1D|AT!r){T2Xvwo+0vAW{&b(ya}o0)Q#q;m=x4+3 zU7gNvK>bt_CDWcP&0>jo$nS3EfVjk+s?Isv{56!u&r6STo(P}cu@ul5=&9NrD1^Up z=zE7th~;w^7v5K<`H)~|WK~#kR6lV1syxthF&w}gXm13SZ*aI>&Vh=mzJh*Cj%CkS zs#)RnBxmG4%X&~U=gkl?%+WHw;mO)06ly+aU5M(&6emn1W>Ussy2iHk8j54JMPpy} z)AhX8`2Q{Z=vW2676EIr(0Y(pv*Aigf|4(0``N5btn{S<6JDGgFsnFfimrd*r=!kJ zyZRv(NttYyVD}P$i8xYXLIOM2S3<45ecnYg{aiUKWFhm%@Gswk8ACp0#*7T&f!WQw;5rL3q^n_U^ zH`eW;QuFW1OqSo`b+XL1HSDV&`jwO|>RU4twRb+24Q#uqLvUys=8I;oY&H<4VN4&s zunnD7(@aeGu_jlQtaL@|dK~=n&f3$inh!E8u*vw;f#+a_+ZmNqlgzrV*^#fwT|<@q zY;C@8noW3XYVUhLmV5if9IQCA?5bk7w8+372cfmC0J~ z=5Tqw`TpCC>n~*Q=~mwxE|oK0Fbtnj*>ZY2`nj-Irl&s!r(Q1|k4jh~*uA$1pxvBz z<9@GraDqp4%zZPMH+f6I!qo0${l4+##KB(0mjBITgH?1)AA}Rya4`01c2(lHqK)Zf z4LX&8F>WZZ+WQ#LKxmg;jW(Dn-Ly__+&7K&N-q?dHTo%D6oLSSJgFFu+b9Z9Q%nxUGR%2N zBD2_%PtOh37Fs;m_*h{9Wu`A?WvHVu(3a56F;LCf3Pd32p)680@uN3^jjHpjMpPKv znNZj1zcrIKcgtBv?E?9ovtpyu&gQ-7nWXb$xv;(#r<2FeHrTH%JWb;B{Lmm#1#=q- z;p#N59x(8F8Co}liVMOYiGkGRUFuo&;nHG0Nnp_#FL~5`Rl6Z9cVWw-sV~cNMs@GV z$i7yb{7uO*_vL0K6`LcR-fdDaep?Cm;=4qo;&00Av=+8ga3qFm8mG-M3L=WvD<~yw zaa65(y#IZZnJo8#^_2<364@9QL#42aGB!suIF{u$W>MkFxmOPM*X)zQn5`H(^z2C* z$cf(`RnlZfu%XWuiU@?9oMg?MfrEL6%r7+)yO#dm!nd!Uq~@PPx30DgFR)41Po zJ9-w~ef!OSp7~Vw?4leZMI0|8<*c*xaG%dj{O(Zn_$gyg{5R3j)@ePOlew>IoHSjE zb<-g~@8eYpe#|bkh!WBrbe{{q_Kd%#LAgH*96F6qcau27g>d`*Qs|8`+6f;^-ZFXJ zgx&{$T<|UAP_u#$AC zGm_Lc38V@U>cWi`%F9+L5QxnIsPKD`?U0e%)t-ge@Sig`_v)NP8mY=*mnIJ-x7Bi1 zgK@^8L0aLbFJ;rERB~C73x@2G#>f8#bFv0M8av|37w-{3=ql)4Hu?o`CYz(w2>M(} zsorH#P7H3#=ZGwy%DKnOuiwaqGP8baPLT8;~ZQ8K6@iUIc9yHu~I5M991a)pR7dfhv5|?Rxr)&SLsG<_AU)C9M1Q_S&}6maPu z7QKyYMlw%{(%eEpL7fA1DOY5pIA7&GV@~pLvbq^sVx5wU8Xyk!@zE^jJEe%&p1E-_ zoT3Dtwo&|F+dOuiv8p9YyI&SaNI-~iBeiySX?6m6f!am z(CdQ2hrG>GITyH7HWS5C3$MOfM{wGis$*72->c+Fvrn`h4(blkEO$Ho`zEC_mD+9I z2)E>XPF6*UjZqMXNH&biebw|SGR)Lan11yu#?~o^`|gtMjZ^9GE>+}W>KD*?_U=eK zxL7rsYx^L*tf^201sSo%{5ae*U0d<6p1j7;6TRu)KkLm-h}t)#9(U$5t^}XeIGk%V zkboBta-v**8N1=}j5xz9^F}7MF084h>1jZ&RU_@3+D~kV zI5&+PFwL^)6HLM1{|3eP@@^Z;J)S#@6yA?8hJEv6+OS(1LCQ22M-Nd*KM6Rm%SI2w z+)C$Dca0Z4dk6a@f2#eZmHs}#{P4hC8zqcI_qKuRLI*qta z7w9`7a6>X^@DAw0s2dDq64e!3cXTXTPE}!&l_V0?TxBA!*1~14$DubdpsR^~0sde0 zIR;%u4Or`lmB+<$B>!_Ix*mG%hW;`c(S;y-uQ}}H74MB|%zZB(9%c;LUH=nUNN9p6 z`DaGedi4w}Xbx-O%MbXT%?0X}`@-WXbr7_)a*E}UrCDQSdN!Ah_}^vZ|ECu;)M_5= UG#0-6w_yR0dM3J6+OFaM2eR{g-T(jq literal 0 HcmV?d00001 diff --git a/components/card/collaboration-details/application-card.tsx b/components/card/collaboration-details/application-card.tsx new file mode 100644 index 00000000..1d1ef2e2 --- /dev/null +++ b/components/card/collaboration-details/application-card.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import Button from '../../ui/button'; +import Carousel from '@/shared-uis/components/carousel/carousel'; +import { useTheme } from '@react-navigation/native'; +import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; +import { faCheck } from '@fortawesome/free-solid-svg-icons'; +import Colors from '@/constants/Colors'; +import { Card } from '@/components/ui/card/secondary'; +import { CardHeader } from '@/components/ui/card/secondary/card-header'; +import { CardActions } from '@/components/ui/card/secondary/card-actions'; +import { CardDescription } from '@/components/ui/card/secondary/card-description'; +import { CardFooter } from '@/components/ui/card/secondary/card-footer'; +import { processRawAttachment } from '@/utils/attachments'; +import { Attachment } from '@/shared-libs/firestore/trendly-pro/constants/attachment'; +import { InfluencerApplication } from '@/types/Collaboration'; +import { formatDistanceToNow } from "date-fns"; +import { convertToKUnits } from '@/utils/conversion'; + +interface ApplicationCardProps { + acceptApplication: () => void; + data: InfluencerApplication; + headerLeftAction?: () => void; + headerRightAction?: () => void; +} + +export const ApplicationCard: React.FC = ({ + acceptApplication, + data, + headerLeftAction, + headerRightAction, +}) => { + const theme = useTheme(); + + return ( + + + processRawAttachment(attachment))} + theme={theme} + /> + + + Accept + + } + /> + + + + ); +}; diff --git a/components/card/collaboration-details/invitation-card.tsx b/components/card/collaboration-details/invitation-card.tsx new file mode 100644 index 00000000..a8f3f049 --- /dev/null +++ b/components/card/collaboration-details/invitation-card.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import Button from '../../ui/button'; +import { Card } from '../../ui/card/secondary'; +import { CardHeader } from '../../ui/card/secondary/card-header'; +import { CardDescription } from '../../ui/card/secondary/card-description'; +import { CardActions } from '../../ui/card/secondary/card-actions'; +import Carousel from '@/shared-uis/components/carousel/carousel'; +import { useTheme } from '@react-navigation/native'; +import { User } from '@/types/User'; +import { processRawAttachment } from '@/utils/attachments'; +import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; +import { faCheck, faPlus } from '@fortawesome/free-solid-svg-icons'; +import Colors from '@/constants/Colors'; +import useInvitation from '@/hooks/use-invitation'; + +interface InvitationCardProps { + checkIfAlreadyInvited: (influencerId: string) => Promise; + data: User; + headerLeftAction?: () => void; + headerRightAction?: () => void; + inviteInfluencer: () => void; +} + +const InvitationCard: React.FC = ({ + checkIfAlreadyInvited, + data, + headerLeftAction, + headerRightAction, + inviteInfluencer, +}) => { + const theme = useTheme(); + const { + isAlreadyInvited, + } = useInvitation({ + checkIfAlreadyInvited, + influencerId: data.id, + }); + + return ( + + + processRawAttachment(attachment)) || []} + theme={theme} + /> + { + if (!isAlreadyInvited) { + inviteInfluencer(); + } + }} + > + + {isAlreadyInvited ? 'Already Invited' : 'Invite'} + + } + /> + + + ); +}; + +export default InvitationCard; diff --git a/components/card/profile-modal/application-card.tsx b/components/card/profile-modal/application-card.tsx new file mode 100644 index 00000000..82cfedde --- /dev/null +++ b/components/card/profile-modal/application-card.tsx @@ -0,0 +1,98 @@ +import React from 'react'; + +import { Card } from '@/components/ui/card/tertiary'; +import { CardHeader } from '@/components/ui/card/tertiary/card-header'; +import { CardMetaData } from '@/components/ui/card/tertiary/card-metadata'; +import { CardFiles } from '@/components/ui/card/tertiary/card-files'; +import { CardFooter } from '@/components/ui/card/tertiary/card-footer'; +import { CardQuestions } from '@/components/ui/card/tertiary/card-questions'; +import { View } from '@/components/theme/Themed'; +import Button from '@/components/ui/button'; +import { Application } from '@/types/Collaboration'; +import { convertToKUnits } from '@/utils/conversion'; +import Colors from '@/constants/Colors'; +import { useTheme } from '@react-navigation/native'; + +interface ApplicationCardProps { + data: Application; + onAccept: () => void; + onReject: () => void; + questions: { + question: string; + answer: string; + }[]; +} + +export const ApplicationCard: React.FC = ({ + data, + onAccept, + onReject, + questions, +}) => { + const theme = useTheme(); + + return ( + + + { + (data.quotation || data.timeline) && ( + + ) + } + { + data.fileAttachments && data.fileAttachments.length > 0 && ( + console.log('File pressed:', file.name)} + /> + ) + } + { + data.answersFromInfluencer && data.answersFromInfluencer.length > 0 && ( + + ) + } + + + +
+ } + /> + + ); +} diff --git a/components/card/profile-modal/invitation-card.tsx b/components/card/profile-modal/invitation-card.tsx new file mode 100644 index 00000000..c6010cc2 --- /dev/null +++ b/components/card/profile-modal/invitation-card.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import { Card } from '@/components/ui/card/tertiary'; +import { CardHeader } from '@/components/ui/card/tertiary/card-header'; +import { CardFooter } from '@/components/ui/card/tertiary/card-footer'; +import Button from '@/components/ui/button'; +import useInvitation from '@/hooks/use-invitation'; +import Colors from '@/constants/Colors'; +import { useTheme } from '@react-navigation/native'; + +interface InvitationCardProps { + checkIfAlreadyInvited: (influencerId: string) => Promise; + influencerId: string | undefined; + onInvite: () => void; +} + +export const InvitationCard: React.FC = ({ + checkIfAlreadyInvited, + influencerId, + onInvite, +}) => { + const { + isAlreadyInvited, + } = useInvitation({ + checkIfAlreadyInvited, + influencerId: influencerId, + }); + + const theme = useTheme(); + + return ( + + + { + if (!isAlreadyInvited) { + onInvite(); + } + }} + size="small" + customStyles={{ + width: '100%', + backgroundColor: Colors(theme).primary, + }} + > + {isAlreadyInvited ? 'Already Invited' : 'Invite Now'} + + } + /> + + ); +} diff --git a/components/collaboration/collaboration-details/ApplicationsTabContent.tsx b/components/collaboration/collaboration-details/ApplicationsTabContent.tsx index 40d354c2..5a67ab22 100644 --- a/components/collaboration/collaboration-details/ApplicationsTabContent.tsx +++ b/components/collaboration/collaboration-details/ApplicationsTabContent.tsx @@ -1,62 +1,83 @@ -import React, { useEffect, useState } from "react"; -import { View, FlatList, ActivityIndicator } from "react-native"; +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { FlatList, ActivityIndicator } from "react-native"; import { useTheme } from "@react-navigation/native"; -import InfluencerCard from "@/components/InfluencerCard"; import { FirestoreDB } from "@/utils/firestore"; -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 { useBreakpoints } from "@/hooks"; +import { + ApplicationCard +} from "@/components/card/collaboration-details/application-card"; +import { + ApplicationCard as ProfileApplicationCard +} from "@/components/card/profile-modal/application-card"; +import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from "@gorhom/bottom-sheet"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useSharedValue } from "react-native-reanimated"; +import ProfileBottomSheet from "@/shared-uis/components/ProfileModal/Profile-Modal"; +import { User } from "@/types/User"; +import BottomSheetContainer from "@/shared-uis/components/bottom-sheet"; +import { List } from "react-native-paper"; +import { useApplications } from "@/hooks/request"; +import { Attachment } from "@/shared-libs/firestore/trendly-pro/constants/attachment"; +import { processRawAttachment } from "@/utils/attachments"; +import { Application, InfluencerApplication } from "@/types/Collaboration"; +import { View } from "@/components/theme/Themed"; const ApplicationsTabContent = (props: any) => { const theme = useTheme(); - const [isVisible, setIsVisible] = useState(false); - const [influencers, setInfluencers] = useState([]); - const [loading, setLoading] = useState(true); - const [selectedApplication, setSelectedApplication] = useState(null); + const [selectedInfluencerApplication, setSelectedInfluencerApplication] = useState(null); + const [isActionModalVisible, setIsActionModalVisible] = useState(false); const { xl, } = useBreakpoints(); - const fetchApplications = async () => { - try { - const applicationRef = collection( - FirestoreDB, - "collaborations", - props.pageID, - "applications" - ); - const applicationFetch = await getDocs(applicationRef); - const applications = applicationFetch.docs.map((doc) => { - return { - ...doc.data(), - id: doc.id, - } as any; - }); - - const influencers = await Promise.all( - applications.map(async (application) => { - const userRef = doc(FirestoreDB, "users", application.userId); - const userFetch = await getDoc(userRef); - return { - ...application, - ...userFetch.data(), - id: userFetch.id, - applicationID: application.id, - } as any; - }) - ); - setInfluencers(influencers); - } catch (error) { - console.error(error); - } finally { - setLoading(false); - } + 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 renderBackdrop = (props: any) => { + return ( + + ); }; + const toggleActionModal = () => { + setIsActionModalVisible(!isActionModalVisible); + } + + const handleActionModalClose = () => { + setIsActionModalVisible(false); + } + + const collaborationQuestions = props.collaboration?.questionsToInfluencers || []; + + const { + fetchApplications, + handleAcceptApplication, + handleRejectApplication, + influencers, + loading, + } = useApplications({ + collaborationId: props.pageID, + data: props.collaboration, + handleActionModalClose, + influencerApplication: selectedInfluencerApplication as InfluencerApplication, + }); + useEffect(() => { fetchApplications(); }, []); @@ -91,20 +112,22 @@ const ApplicationsTabContent = (props: any) => { ( - { - setIsVisible(true); - setSelectedApplication({ - applicationID: item.applicationID, - collaborationID: props.pageID, - influencerID: item.id, - }); + { + setSelectedInfluencerApplication(item); + setTimeout(() => { + bottomSheetModalRef.current?.present(); + }, 500); + }} + headerRightAction={() => { + setSelectedInfluencerApplication(item); + setIsActionModalVisible(true); }} /> )} - keyExtractor={(item, index) => item.id + index} + keyExtractor={(item, index) => item.application.id + index} style={{ paddingBottom: 16, width: xl ? 640 : '100%', @@ -121,21 +144,77 @@ const ApplicationsTabContent = (props: any) => { ) } /> + { - isVisible && ( - setIsVisible(false)} - snapPointsRange={["30%", "50%"]} - cardId={selectedApplication} - key={selectedApplication?.applicationID} - /> + isActionModalVisible && ( + + + { + handleAcceptApplication(); + }} + /> + { + handleRejectApplication(); + }} + /> + + ) } + + + + + { + handleRejectApplication(); + }} + onAccept={() => { + handleAcceptApplication(); + }} + questions={selectedInfluencerApplication?.application.answersFromInfluencer.map((answer) => ({ + question: collaborationQuestions[answer.question], + answer: answer.answer + })) || []} + /> +
+ } + carouselMedia={selectedInfluencerApplication?.application.attachments.map((attachment: Attachment) => processRawAttachment(attachment))} + FireStoreDB={FirestoreDB} + influencer={selectedInfluencerApplication?.influencer as User} + isBrandsApp={true} + theme={theme} + /> + + ); }; diff --git a/components/collaboration/collaboration-details/InvitationsTabContent.tsx b/components/collaboration/collaboration-details/InvitationsTabContent.tsx index 3018683e..091e288c 100644 --- a/components/collaboration/collaboration-details/InvitationsTabContent.tsx +++ b/components/collaboration/collaboration-details/InvitationsTabContent.tsx @@ -1,6 +1,5 @@ -import React, { useState } from "react"; +import React, { useMemo, useRef, useState } from "react"; import { - View, FlatList, Modal, TextInput, @@ -9,28 +8,43 @@ import { import { Text, Button, + List, } from "react-native-paper"; import { useTheme } from "@react-navigation/native"; import { stylesFn } from "@/styles/collaboration-details/CollaborationDetails.styles"; -import InfluencerCard from "@/components/InfluencerCard"; import { FirestoreDB } from "@/utils/firestore"; import { addDoc, collection } from "firebase/firestore"; -import BottomSheetActions from "@/components/BottomSheetActions"; import { AuthApp } from "@/utils/auth"; import Toaster from "@/shared-uis/components/toaster/Toaster"; import { useInfluencers } from "@/hooks/request"; import EmptyState from "@/components/ui/empty-state"; import Colors from "@/constants/Colors"; import { useBreakpoints } from "@/hooks"; +import BottomSheetContainer from "@/shared-uis/components/bottom-sheet"; +import InvitationCard from "@/components/card/collaboration-details/invitation-card"; +import { + InvitationCard as ProfileInvitationCard +} from "@/components/card/profile-modal/invitation-card"; +import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from "@gorhom/bottom-sheet"; +import ProfileBottomSheet from "@/shared-uis/components/ProfileModal/Profile-Modal"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useSharedValue } from "react-native-reanimated"; +import { processRawAttachment } from "@/utils/attachments"; +import { Attachment } from "@/shared-libs/firestore/trendly-pro/constants/attachment"; +import { User } from "@/types/User"; +import { View } from "@/components/theme/Themed"; const InvitationsTabContent = (props: any) => { const theme = useTheme(); const styles = stylesFn(theme); - const [isVisible, setIsVisible] = useState(false); - const [selectedInfluencer, setSelectedInfluencer] = useState(null); - const [isMessageModalVisible, setIsMessageModalVisible] = useState(false); + const [isActionModalVisible, setIsActionModalVisible] = useState(false); + const [selectedInfluencer, setSelectedInfluencer] = useState(null); + const [isInvitationModalVisible, setIsInvitationModalVisible] = useState(false); const [message, setMessage] = useState(""); const [isInviting, setIsInviting] = useState(false); + + const collaborationId = props.pageID; + const { isInitialLoading, checkIfAlreadyInvited, @@ -40,33 +54,64 @@ const InvitationsTabContent = (props: any) => { influencers, isLoading, } = useInfluencers({ - pageID: props.pageID, + collaborationId, }); const { xl, } = useBreakpoints(); + 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 renderBackdrop = (props: any) => { + return ( + + ); + }; + + const toggleActionModal = () => { + setIsActionModalVisible(!isActionModalVisible); + } + + const handleActionModalClose = () => { + setIsActionModalVisible(false); + } + const handleCollaborationInvite = async () => { try { + if (!selectedInfluencer) return; + setIsInviting(true); const invitationRef = collection( FirestoreDB, "collaborations", - props.pageID, + collaborationId, "invitations" ); const invitationPayload = { - userId: selectedInfluencer.influencerID, + userId: selectedInfluencer.id, managerId: AuthApp.currentUser?.uid, - collaborationId: selectedInfluencer.collaborationID, + collaborationId, status: "pending", message: message, }; await addDoc(invitationRef, invitationPayload).then(() => { - setIsMessageModalVisible(false); + setIsInvitationModalVisible(false); Toaster.success("Invitation sent successfully"); }); } catch (error) { @@ -113,33 +158,21 @@ const InvitationsTabContent = (props: any) => { }} data={influencers} renderItem={({ item }) => ( - { + setSelectedInfluencer(item); + setTimeout(() => { + bottomSheetModalRef.current?.present(); + }, 500); }} - ToggleModal={() => { - setIsVisible(true); - setSelectedInfluencer({ - collaborationID: props.pageID, - influencerID: item.id, - }); + headerRightAction={() => { + setSelectedInfluencer(item); + setIsActionModalVisible(true); }} - ToggleMessageModal={() => { - setIsMessageModalVisible(true); - setSelectedInfluencer({ - collaborationID: props.pageID, - influencerID: item.id, - }); + inviteInfluencer={() => { + setIsInvitationModalVisible(true); }} /> )} @@ -169,12 +202,13 @@ const InvitationsTabContent = (props: any) => { ) } /> + setIsMessageModalVisible(false)} - onRequestClose={() => setIsMessageModalVisible(false)} + onDismiss={() => setIsInvitationModalVisible(false)} + onRequestClose={() => setIsInvitationModalVisible(false)} > @@ -196,7 +230,7 @@ const InvitationsTabContent = (props: any) => { @@ -204,18 +238,76 @@ const InvitationsTabContent = (props: any) => { + { - isVisible && ( - setIsVisible(false)} - snapPointsRange={["30%", "80%"]} - cardId={selectedInfluencer} - key={selectedInfluencer.collaborationID} - /> + isActionModalVisible && ( + + + { + setIsActionModalVisible(false); + setTimeout(() => { + setIsInvitationModalVisible(true); + }, 200); + }} + /> + { + console.log("Message User"); + }} + /> + + ) } + + + + + { + if (!selectedInfluencer) return; + + setIsInvitationModalVisible(true); + }} + /> +
+ } + carouselMedia={selectedInfluencer?.profile?.attachments?.map((attachment: Attachment) => processRawAttachment(attachment))} + FireStoreDB={FirestoreDB} + influencer={selectedInfluencer as User} + isBrandsApp={true} + theme={theme} + /> + + ); }; diff --git a/components/collaboration/collaboration-details/index.tsx b/components/collaboration/collaboration-details/index.tsx index 8e15e572..21306ad7 100644 --- a/components/collaboration/collaboration-details/index.tsx +++ b/components/collaboration/collaboration-details/index.tsx @@ -103,6 +103,7 @@ const CollaborationDetails: React.FC = ({ collaboration={{ id: pageID, name: collaboration?.name, + questionsToInfluencers: collaboration?.questionsToInfluencers || [], }} /> ), diff --git a/components/collaborations/index.tsx b/components/collaborations/index.tsx index 17c96d22..2318b103 100644 --- a/components/collaborations/index.tsx +++ b/components/collaborations/index.tsx @@ -3,6 +3,10 @@ import AppLayout from "@/layouts/app-layout"; import Applications from "@/components/proposals/Applications"; import Invitations from "@/components/proposals/Invitations"; import TopTabNavigation from "../ui/top-tab-navigation"; +import Button from "../ui/button"; +import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { useRouter } from "expo-router"; const tabs = [ { @@ -18,17 +22,45 @@ const tabs = [ ]; const Collaborations = () => { + const router = useRouter(); + return ( + ); diff --git a/components/drawer-layout/DrawerMenuContent.tsx b/components/drawer-layout/DrawerMenuContent.tsx index 04d2d97b..92a7a075 100644 --- a/components/drawer-layout/DrawerMenuContent.tsx +++ b/components/drawer-layout/DrawerMenuContent.tsx @@ -15,15 +15,15 @@ import { useEffect, useState } from "react"; import { Searchbar } from "react-native-paper"; import { faComment, + faFileLines, faStar, } from "@fortawesome/free-regular-svg-icons"; import { faComment as faCommentSolid, - faGears, + faBuilding, faHouseUser as faHouseUserSolid, faMagnifyingGlass, faPlus, - faPlusCircle, faStar as faStarSolid, } from "@fortawesome/free-solid-svg-icons"; import stylesFn from "@/styles/searchbar/Searchbar.styles"; @@ -37,66 +37,66 @@ const DRAWER_MENU_CONTENT_ITEMS = ( theme: Theme, ) => [ { - href: "/explore-influencers", + href: "/collaborations", icon: ({ focused, }: IconPropFn) => focused ? ( ) : ( - ), - label: "Home", + label: "Collaborations", }, { - href: "/collaborations", + href: "/messages", icon: ({ focused, }: IconPropFn) => focused ? ( ) : ( ), - label: "Collaborations", + label: "Messages", }, { - href: "/create-collaboration", + href: "/contracts", icon: () => ( ), - label: "Create Collaboration", + label: "Contracts", }, { - href: "/messages", + href: "/explore-influencers", icon: ({ focused, }: IconPropFn) => focused ? ( ) : ( - ), - label: "Messages", + label: "Explore", }, { href: "/menu", @@ -105,15 +105,15 @@ const DRAWER_MENU_CONTENT_ITEMS = ( }: IconPropFn) => focused ? ( ) : ( ), - label: "Settings", + label: "My Brand", }, ]; diff --git a/components/ui/card/secondary/card-actions.tsx b/components/ui/card/secondary/card-actions.tsx new file mode 100644 index 00000000..3bdf944c --- /dev/null +++ b/components/ui/card/secondary/card-actions.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { Text, View } from '@/components/theme/Themed'; +import Colors from '@/constants/Colors'; +import { faChartLine, faFaceSmile, faPeopleRoof } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; +import { useTheme } from '@react-navigation/native'; +import { convertToKUnits } from '@/utils/conversion'; + +type CardActionsProps = { + metrics: { + followers: number; + reach: number; + rating: number; + }; + action?: React.ReactNode; +}; + +export const CardActions = ({ + metrics, + action = null, +}: CardActionsProps) => { + const theme = useTheme(); + + return ( + + + + + {convertToKUnits(metrics.followers)} + + + + {convertToKUnits(metrics.reach)} + + + + {convertToKUnits(metrics.rating)} + + + {action} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 16, + }, + metrics: { + flexDirection: 'row', + alignItems: 'center', + }, + metric: { + flexDirection: 'row', + alignItems: 'center', + marginRight: 16, + }, + metricText: { + marginLeft: 4, + fontSize: 14, + }, +}); diff --git a/components/ui/card/secondary/card-description.tsx b/components/ui/card/secondary/card-description.tsx new file mode 100644 index 00000000..1b6e2e2c --- /dev/null +++ b/components/ui/card/secondary/card-description.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { Text, View } from '@/components/theme/Themed'; + +type CardDescriptionProps = { + text: string; +}; + +export const CardDescription = ({ text }: CardDescriptionProps) => { + return ( + + + {text} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + paddingHorizontal: 16, + }, + text: { + fontSize: 14, + lineHeight: 20, + }, +}); diff --git a/components/ui/card/secondary/card-footer.tsx b/components/ui/card/secondary/card-footer.tsx new file mode 100644 index 00000000..a955ab20 --- /dev/null +++ b/components/ui/card/secondary/card-footer.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { Text, View } from '@/components/theme/Themed'; +import { CURRENCY_SYMBOL } from '@/constants/Unit'; + +type CardFooterProps = { + quote: string; + timeline: string; +}; + +export const CardFooter = ({ + quote, + timeline, +}: CardFooterProps) => { + return ( + + + Quote: {CURRENCY_SYMBOL}{quote} + Timeline: {timeline} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + paddingHorizontal: 16, + }, + footer: { + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: 8, + }, + footerText: { + fontSize: 14, + }, +}); diff --git a/components/ui/card/secondary/card-header.tsx b/components/ui/card/secondary/card-header.tsx new file mode 100644 index 00000000..59eb9976 --- /dev/null +++ b/components/ui/card/secondary/card-header.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import { Image, StyleSheet, Pressable } from 'react-native'; +import { Theme, useTheme } from '@react-navigation/native'; +import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; +import { faEllipsis } from '@fortawesome/free-solid-svg-icons'; +import Colors from '@/constants/Colors'; +import { Avatar } from 'react-native-paper'; +import { imageUrl } from '@/utils/url'; +import { Text, View } from '@/components/theme/Themed'; + +type CardHeaderProps = { + avatar: string; + handle?: string; + isVerified?: boolean; + leftAction?: () => void; + name: string; + rightAction?: () => void; + timestamp?: string; +}; + +export const CardHeader = ({ + avatar, + handle, + isVerified = false, + leftAction, + name, + rightAction, + timestamp, +}: CardHeaderProps) => { + const theme = useTheme(); + const styles = stylesFn(theme); + + return ( + + + {avatar ? ( + + ) : ( + + {name?.[0] || 'U'} + + )} + + + {name} + {isVerified && ( + + )} + + {handle || '@socialmedia'} + + + + {timestamp && {timestamp}} + + + + + + ); +}; + +const stylesFn = (theme: Theme) => StyleSheet.create({ + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + }, + leftContent: { + flexDirection: 'row', + alignItems: 'center', + }, + avatar: { + width: 48, + height: 48, + borderRadius: 100, + }, + avatarPlaceholder: { + width: 48, + height: 48, + borderRadius: 100, + backgroundColor: Colors(theme).aliceBlue, + alignItems: 'center', + justifyContent: 'center', + }, + avatarText: { + fontSize: 16, + color: Colors(theme).primary, + }, + userInfo: { + marginLeft: 12, + }, + nameContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 2, + }, + name: { + fontSize: 16, + fontWeight: '600', + }, + verifiedBadge: { + marginLeft: 4, + width: 22, + height: 22, + }, + handle: { + fontSize: 14, + }, + timestamp: { + fontSize: 14, + }, +}); diff --git a/components/ui/card/secondary/index.tsx b/components/ui/card/secondary/index.tsx new file mode 100644 index 00000000..96538172 --- /dev/null +++ b/components/ui/card/secondary/index.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { Theme, useTheme } from '@react-navigation/native'; + +import { View } from '@/components/theme/Themed'; +import Colors from '@/constants/Colors'; + +type CardProps = { + children: React.ReactNode; +}; + +export const Card = ({ + children, +}: CardProps) => { + const theme = useTheme(); + const styles = stylesFn(theme); + + return ( + {children} + ); +}; + +const stylesFn = (theme: Theme) => StyleSheet.create({ + card: { + backgroundColor: Colors(theme).card, + gap: 16, + paddingTop: 16, + paddingBottom: 24, + }, +}); diff --git a/components/ui/card/tertiary/card-files.tsx b/components/ui/card/tertiary/card-files.tsx new file mode 100644 index 00000000..41b7b608 --- /dev/null +++ b/components/ui/card/tertiary/card-files.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { TouchableOpacity, StyleSheet } from 'react-native'; +import { Theme, useTheme } from '@react-navigation/native'; +import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; +import { faPaperclip } from '@fortawesome/free-solid-svg-icons'; + +import Colors from '@/constants/Colors'; +import { Text, View } from '@/components/theme/Themed'; + +type File = { + url: string; + name: string; + type: string; +}; + +type CardFilesProps = { + files: File[]; + onFilePress?: (file: File) => void; +}; + +export const CardFiles = ({ files, onFilePress }: CardFilesProps) => { + const theme = useTheme(); + const styles = stylesFn(theme); + + return ( + + {files.map((file, index) => ( + onFilePress?.(file)} + > + + {file.name} + + ))} + + ); +}; + +const stylesFn = (theme: Theme) => StyleSheet.create({ + container: { + gap: 16, + backgroundColor: Colors(theme).transparent, + }, + fileRow: { + flexDirection: 'row', + alignItems: 'center', + }, + fileName: { + marginLeft: 12, + fontSize: 16, + textDecorationLine: 'underline', + }, +}); diff --git a/components/ui/card/tertiary/card-footer.tsx b/components/ui/card/tertiary/card-footer.tsx new file mode 100644 index 00000000..8c90c034 --- /dev/null +++ b/components/ui/card/tertiary/card-footer.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +import { View } from '@/components/theme/Themed'; + +type CardFooterProps = { + footerActions?: React.ReactNode; +}; + +export const CardFooter = ({ + footerActions = null, +}: CardFooterProps) => { + return ( + + {footerActions} + + ); +}; diff --git a/components/ui/card/tertiary/card-header.tsx b/components/ui/card/tertiary/card-header.tsx new file mode 100644 index 00000000..fb11b761 --- /dev/null +++ b/components/ui/card/tertiary/card-header.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; + +import { Text, View } from '@/components/theme/Themed'; +import { Theme, useTheme } from '@react-navigation/native'; +import Colors from '@/constants/Colors'; + +type CardHeaderProps = { + title: string; + description?: string; +}; + +export const CardHeader = ({ + title, + description +}: CardHeaderProps) => { + const theme = useTheme(); + const styles = stylesFn(theme); + + return ( + + {title} + {description} + + ); +}; + +const stylesFn = (theme: Theme) => StyleSheet.create({ + header: { + backgroundColor: Colors(theme).transparent, + gap: 16, + }, + title: { + fontSize: 18, + fontWeight: '600', + }, + description: { + fontSize: 16, + lineHeight: 24, + }, +}); diff --git a/components/ui/card/tertiary/card-metadata.tsx b/components/ui/card/tertiary/card-metadata.tsx new file mode 100644 index 00000000..ff536a0b --- /dev/null +++ b/components/ui/card/tertiary/card-metadata.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { Theme, useTheme } from '@react-navigation/native'; + +import { Text, View } from '@/components/theme/Themed'; +import { CURRENCY_SYMBOL } from '@/constants/Unit'; +import Colors from '@/constants/Colors'; + +type CardMetaDataProps = { + quote?: string; + timeline?: string; +}; + +export const CardMetaData = ({ quote, timeline }: CardMetaDataProps) => { + const theme = useTheme(); + const styles = stylesFn(theme); + + return ( + + + {quote && Quote: {CURRENCY_SYMBOL}{quote}} + {timeline && Timeline: {timeline}} + + + ); +}; + +const stylesFn = (theme: Theme) => StyleSheet.create({ + container: { + backgroundColor: Colors(theme).transparent, + gap: 16, + }, + metaContainer: { + backgroundColor: Colors(theme).transparent, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + meta: { + fontSize: 16, + fontWeight: '500', + }, +}); diff --git a/components/ui/card/tertiary/card-questions.tsx b/components/ui/card/tertiary/card-questions.tsx new file mode 100644 index 00000000..281a7908 --- /dev/null +++ b/components/ui/card/tertiary/card-questions.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { View, Text } from '@/components/theme/Themed'; +import Colors from '@/constants/Colors'; +import { Theme, useTheme } from '@react-navigation/native'; + +type Question = { + question: string; + answer: string; +}; + +type CardQuestionsProps = { + questions: Question[]; +}; + +export const CardQuestions = ({ questions }: CardQuestionsProps) => { + const theme = useTheme(); + const styles = stylesFn(theme); + + return ( + + {questions.map((item, index) => ( + + {item.question} + {item.answer} + + ))} + + ); +}; + +const stylesFn = (theme: Theme) => StyleSheet.create({ + container: { + backgroundColor: Colors(theme).transparent, + gap: 16, + }, + questionContainer: { + backgroundColor: Colors(theme).transparent, + }, + question: { + fontSize: 16, + fontWeight: '600', + marginBottom: 8, + }, + answer: { + fontSize: 16, + lineHeight: 24, + }, +}); diff --git a/components/ui/card/tertiary/index.tsx b/components/ui/card/tertiary/index.tsx new file mode 100644 index 00000000..4c7ec1a2 --- /dev/null +++ b/components/ui/card/tertiary/index.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { Theme, useTheme } from '@react-navigation/native'; + +import Colors from '@/constants/Colors'; +import { View } from '@/components/theme/Themed'; + +type CardProps = { + children: React.ReactNode; +}; + +export const Card = ({ + children, +}: CardProps) => { + const theme = useTheme(); + const styles = stylesFn(theme); + + return ( + {children} + ); +}; + +const stylesFn = (theme: Theme) => StyleSheet.create({ + card: { + gap: 24, + paddingHorizontal: 16, + paddingVertical: 24, + borderRadius: 16, + borderWidth: 1, + borderColor: Colors(theme).border, + }, +}); diff --git a/constants/Unit.ts b/constants/Unit.ts new file mode 100644 index 00000000..ddbf642d --- /dev/null +++ b/constants/Unit.ts @@ -0,0 +1,2 @@ +export const CURRENCY = "Rs"; +export const CURRENCY_SYMBOL = "₹"; diff --git a/contexts/collaboration-context.provider.tsx b/contexts/collaboration-context.provider.tsx index 5e90213f..38811410 100644 --- a/contexts/collaboration-context.provider.tsx +++ b/contexts/collaboration-context.provider.tsx @@ -43,6 +43,7 @@ export const CollaborationContextProvider: React.FC = ({ collaboration: Partial, ): Promise => { const collaborationRef = collection(FirestoreDB, "collaborations"); + await addDoc(collaborationRef, collaboration); } diff --git a/hooks/request/index.ts b/hooks/request/index.ts index 7b415fcd..ea6701c5 100644 --- a/hooks/request/index.ts +++ b/hooks/request/index.ts @@ -1,3 +1,4 @@ import useInfluencers from "./use-influencers"; +import useApplications from "./use-applications"; -export { useInfluencers }; +export { useApplications, useInfluencers }; diff --git a/hooks/request/use-applications.tsx b/hooks/request/use-applications.tsx new file mode 100644 index 00000000..683a9d8d --- /dev/null +++ b/hooks/request/use-applications.tsx @@ -0,0 +1,155 @@ +import { useState } from "react"; +import { useRouter } from "expo-router"; +import { collection, getDocs, doc, updateDoc, getDoc } from "firebase/firestore"; + +import { useChatContext, useNotificationContext } from "@/contexts"; +import Toaster from "@/shared-uis/components/toaster/Toaster"; +import { FirestoreDB } from "@/utils/firestore"; +import { Application, InfluencerApplication } from "@/types/Collaboration"; +import { User } from "@/types/User"; + +interface UseApplicationsProps { + collaborationId: string; + data: any; + handleActionModalClose: () => void; + influencerApplication: InfluencerApplication; +} + +const useApplications = ({ + collaborationId, + data, + handleActionModalClose, + influencerApplication, +}: UseApplicationsProps) => { + const { createGroupWithMembers, connectUser } = useChatContext(); + const { createNotification } = useNotificationContext(); + const router = useRouter(); + const [influencers, setInfluencers] = useState<{ + influencer: User; + application: Application; + }[]>([]); + const [loading, setLoading] = useState(true); + + const fetchApplications = async () => { + try { + const applicationRef = collection( + FirestoreDB, + "collaborations", + collaborationId, + "applications" + ); + const applicationFetch = await getDocs(applicationRef); + const applications = applicationFetch.docs.map((doc) => { + return { + ...doc.data(), + id: doc.id, + } as Application; + }); + + const influencers = await Promise.all( + applications.map(async (application) => { + const userRef = doc(FirestoreDB, "users", application.userId); + const userFetch = await getDoc(userRef); + return { + application: { + ...application, + id: application.id, + }, + influencer: { + ...userFetch.data(), + id: userFetch.id, + } + } as InfluencerApplication; + }) + ); + + setInfluencers(influencers); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }; + + const handleAcceptApplication = async () => { + try { + if (!influencerApplication.application) return; + + const applicationRef = doc( + FirestoreDB, + "collaborations", + influencerApplication.application.collaborationId, + "applications", + influencerApplication.application.id, + ); + await updateDoc(applicationRef, { + status: "accepted", + }).then(() => { + createGroupWithMembers(data.collaboration.name, [ + influencerApplication.application.userId, + ]).then((channel) => { + connectUser(); + + createNotification( + influencerApplication.application.userId, + { + data: { + collaborationId: influencerApplication.application.collaborationId, + }, + title: "Application Accepted", + description: `Your application for ${data.collaboration.name} has been accepted`, + timeStamp: Date.now(), + isRead: false, + type: "application-accepted", + }, + "users" + ); + + router.navigate(`/channel/${channel.cid}`); + }); + + handleActionModalClose(); + + Toaster.success("Application accepted successfully"); + }); + } catch (error) { + console.error(error); + handleActionModalClose(); + Toaster.error("Failed to accept application"); + } + }; + + const handleRejectApplication = async () => { + try { + if (!influencerApplication.application) return; + + const applicationRef = doc( + FirestoreDB, + "collaborations", + influencerApplication.application.collaborationId, + "applications", + influencerApplication.application.id + ); + await updateDoc(applicationRef, { + status: "rejected", + }).then(() => { + handleActionModalClose(); + Toaster.success("Application rejected successfully"); + }); + } catch (error) { + console.error(error); + handleActionModalClose(); + Toaster.error("Failed to reject application"); + } + }; + + return { + handleAcceptApplication, + handleRejectApplication, + fetchApplications, + influencers, + loading, + }; +} + +export default useApplications; diff --git a/hooks/request/use-influencers.tsx b/hooks/request/use-influencers.tsx index 10c19e34..4988329d 100644 --- a/hooks/request/use-influencers.tsx +++ b/hooks/request/use-influencers.tsx @@ -3,11 +3,11 @@ import { useEffect, useState } from "react"; import { collection, getDocs, where, query, orderBy, limit, onSnapshot, QuerySnapshot, DocumentData, startAfter } from "firebase/firestore"; interface UseInfluencersProps { - pageID: string; + collaborationId: string; } const useInfluencers = ({ - pageID, + collaborationId, }: UseInfluencersProps) => { const [influencers, setInfluencers] = useState([]); const [lastVisible, setLastVisible] = useState(null); @@ -86,7 +86,7 @@ const useInfluencers = ({ const invitationsRef = collection( FirestoreDB, "collaborations", - pageID, + collaborationId, "invitations" ); diff --git a/hooks/use-invitation.ts b/hooks/use-invitation.ts new file mode 100644 index 00000000..9af050e1 --- /dev/null +++ b/hooks/use-invitation.ts @@ -0,0 +1,27 @@ +import { useEffect, useState } from "react"; + +interface UseInvitationProps { + checkIfAlreadyInvited: (influencerId: string) => Promise; + influencerId: string | undefined; +} + +const useInvitation = ({ + checkIfAlreadyInvited, + influencerId, +}: UseInvitationProps) => { + const [isAlreadyInvited, setIsAlreadyInvited] = useState(false); + + useEffect(() => { + if (checkIfAlreadyInvited && influencerId) { + checkIfAlreadyInvited(influencerId).then((invited) => { + setIsAlreadyInvited(invited); + }); + } + }, []); + + return { + isAlreadyInvited, + }; +}; + +export default useInvitation; diff --git a/shared-libs b/shared-libs index 197bdf8a..5cbb5197 160000 --- a/shared-libs +++ b/shared-libs @@ -1 +1 @@ -Subproject commit 197bdf8ae9c3e18ce1f7cd1b31658719a82666f0 +Subproject commit 5cbb5197b939f1bb52a34295499b95fc7d792d1a diff --git a/shared-uis b/shared-uis index 82730e0e..0bde09fd 160000 --- a/shared-uis +++ b/shared-uis @@ -1 +1 @@ -Subproject commit 82730e0e58f838deee23459bb40b0c4a732f576d +Subproject commit 0bde09fd1a00bd6135b24ea6f44ae8477b3712fe diff --git a/types/Collaboration.ts b/types/Collaboration.ts index 00b78566..410bb48b 100644 --- a/types/Collaboration.ts +++ b/types/Collaboration.ts @@ -1,5 +1,23 @@ -import { ICollaboration } from "@/shared-libs/firestore/trendly-pro/models/collaborations"; +import { + IApplications, + ICollaboration, + IInvitations, +} from "@/shared-libs/firestore/trendly-pro/models/collaborations"; +import { User } from "./User"; export interface Collaboration extends ICollaboration { id: string; } + +export interface Application extends IApplications { + id: string; +} + +export interface Invitation extends IInvitations { + id: string; +} + +export type InfluencerApplication = { + application: Application; + influencer: User; +}; From 7f946c42767585412faf868aa4a142dcdb5206a0 Mon Sep 17 00:00:00 2001 From: Manan Sharma <112090668+Manan-Sharma-5@users.noreply.github.com> Date: Sun, 5 Jan 2025 12:07:00 +0530 Subject: [PATCH 04/17] added keyboard view --- components/contracts/FeedbackModal.tsx | 186 ++++++++++++++----------- styles/CollaborationDetails.styles.tsx | 1 + 2 files changed, 102 insertions(+), 85 deletions(-) diff --git a/components/contracts/FeedbackModal.tsx b/components/contracts/FeedbackModal.tsx index 79f7856a..c330d0cb 100644 --- a/components/contracts/FeedbackModal.tsx +++ b/components/contracts/FeedbackModal.tsx @@ -1,5 +1,14 @@ import React, { useState } from "react"; -import { Image, Pressable, Linking, StyleSheet, TextInput } from "react-native"; +import { + Image, + Pressable, + Linking, + StyleSheet, + TextInput, + KeyboardAvoidingView, + Platform, + Keyboard, +} from "react-native"; import { useTheme } from "@react-navigation/native"; import { Text, View } from "@/components/theme/Themed"; import { Chip, Modal, Portal } from "react-native-paper"; @@ -42,6 +51,7 @@ const FeedbackModal: React.FC = ({ ); if (textFeedback === "" || selectedStar === 0) { alert("Please provide feedback and rating before submitting"); + return; } const date = new Date(); await updateDoc(contractRef, { @@ -76,96 +86,101 @@ const FeedbackModal: React.FC = ({ marginHorizontal: 20, }} > - - - Feedback - setVisibility(false)}> - - - - - - {feedbackGiven - ? "Thank you for your feedback!" - : "Please provide your feedback"} - - {star === 0 && ( - - {[1, 2, 3, 4, 5].map((i) => ( - setSelectedStar(i)}> - - - ))} - - )} - {!feedbackGiven && ( - setTextFeedback(text)} - numberOfLines={5} - multiline - /> - )} - {!feedbackGiven && ( - { - provideFeedback(); - }} - > - + Keyboard.dismiss()}> + + Feedback + setVisibility(false)}> + + + + + + {feedbackGiven + ? "Thank you for your feedback!" + : "Please provide your feedback"} + + {star === 0 && ( + + {[1, 2, 3, 4, 5].map((i) => ( + setSelectedStar(i)}> + + + ))} + + )} + {!feedbackGiven && ( + setTextFeedback(text)} + numberOfLines={5} + multiline + /> + )} + {!feedbackGiven && ( + { + provideFeedback(); }} > - Submit Feedback - - - )} - {feedbackGiven && ( - setVisibility(false)} - > - + Submit Feedback + + + )} + {feedbackGiven && ( + setVisibility(false)} > - Close - - - )} - - + + Close + + + )} +
+ + ); }; @@ -198,6 +213,7 @@ const styles = StyleSheet.create({ flexDirection: "row", marginVertical: 10, }, + keyboardAvoidingView: {}, textInput: { width: "100%", height: 100, diff --git a/styles/CollaborationDetails.styles.tsx b/styles/CollaborationDetails.styles.tsx index d1166f3a..61223820 100644 --- a/styles/CollaborationDetails.styles.tsx +++ b/styles/CollaborationDetails.styles.tsx @@ -26,6 +26,7 @@ export const stylesFn = (theme: Theme) => }, name: { fontWeight: "bold", + width: "50%", fontSize: 20, color: Colors(theme).text, lineHeight: 20, From 6d9fbc0487bf36bff3cd8bc5c90939b3bc0ee573 Mon Sep 17 00:00:00 2001 From: Manan Sharma <112090668+Manan-Sharma-5@users.noreply.github.com> Date: Sun, 5 Jan 2025 12:55:29 +0530 Subject: [PATCH 05/17] fixes --- components/contract-card/UserResponse.tsx | 39 ++++++++++++++++------- components/profile/index.tsx | 36 +++++++++++++++++++-- package-lock.json | 10 ++++++ package.json | 3 +- 4 files changed, 73 insertions(+), 15 deletions(-) diff --git a/components/contract-card/UserResponse.tsx b/components/contract-card/UserResponse.tsx index 472f0c47..c9056248 100644 --- a/components/contract-card/UserResponse.tsx +++ b/components/contract-card/UserResponse.tsx @@ -1,15 +1,13 @@ import { IApplications } from "@/shared-libs/firestore/trendly-pro/models/collaborations"; import { FC } from "react"; import { Text, View } from "../theme/Themed"; -import { Button } from "react-native-paper"; -import { Pressable, ScrollView } from "react-native"; +import { Platform, Pressable, ScrollView } from "react-native"; import RenderMediaItem from "@/shared-uis/components/carousel/render-media-item"; import { processRawAttachment } from "@/utils/attachments"; import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"; import { faPaperclip } from "@fortawesome/free-solid-svg-icons"; -import { formatDistanceToNow } from "date-fns"; -import { router } from "expo-router"; -import Colors from "@/constants/Colors"; +import * as FileSystem from "expo-file-system"; +import * as Sharing from "expo-sharing"; import { useTheme } from "@react-navigation/native"; interface UserResponseProps { @@ -28,6 +26,24 @@ const UserResponse: FC = ({ }); const theme = useTheme(); + const downloadAndSaveFile = async (url: string, filename: string) => { + try { + const { uri } = await FileSystem.downloadAsync( + url, + FileSystem.documentDirectory + filename + ); + if (Platform.OS === "web") { + Sharing.shareAsync(uri); + } else { + await Sharing.shareAsync(uri); + } + } catch (error) { + console.error("Error downloading file:", error); + setConfirmationModalVisible(false); + alert("Failed to download file. Please try again."); + } + }; + return ( = ({ {application?.fileAttachments && application.fileAttachments.map((attachment, index) => { return ( - + downloadAndSaveFile(attachment.url, attachment.name) + } + style={{ flexDirection: "row", alignItems: "center", gap: 10 }} > = ({ > {attachment.name} - + ); })} diff --git a/components/profile/index.tsx b/components/profile/index.tsx index 0505786e..d6985342 100644 --- a/components/profile/index.tsx +++ b/components/profile/index.tsx @@ -9,7 +9,7 @@ import { ActivityIndicator, } from "react-native"; import { TextInput, Button, Avatar } from "react-native-paper"; -import { doc, updateDoc } from "firebase/firestore"; +import { doc, getDoc, updateDoc } from "firebase/firestore"; import Colors from "@/constants/Colors"; import { PLACEHOLDER_PERSON_IMAGE } from "@/constants/Placeholder"; import { useAuthContext, useFirebaseStorageContext } from "@/contexts"; @@ -28,6 +28,7 @@ import { uploadBytes, } from "firebase/storage"; import { StorageApp } from "@/utils/firebase-storage"; +import { useBrandContext } from "@/contexts/brand-context.provider"; const Profile = () => { const [name, setName] = useState(""); @@ -36,6 +37,7 @@ const Profile = () => { const [isModalVisible, setIsModalVisible] = useState(false); const [loading, setLoading] = useState(false); // Loader state const { manager } = useAuthContext(); + const { selectedBrand } = useBrandContext(); const [capturedImage, setCapturedImage] = useState( manager?.profileImage || "" ); @@ -73,6 +75,18 @@ const Profile = () => { profileImage: updatedImageURL, name: name || manager.name, }); + if (selectedBrand) { + const brandMemberRef = doc( + FirestoreDB, + "brands", + selectedBrand.id, + "members", + manager.id + ); + await updateDoc(brandMemberRef, { + role, + }); + } setImageToUpload(""); // Clear image-to-upload buffer Toaster.success("Profile updated successfully"); @@ -84,13 +98,29 @@ const Profile = () => { } }; + const fetchRole = async () => { + if (selectedBrand && selectedBrand.id && manager && manager.id) { + const managerRef = doc( + FirestoreDB, + "brands", + selectedBrand.id, + "members", + manager.id + ); + const managerData = await getDoc(managerRef); + if (managerData.exists()) { + setRole(managerData.data().role); + } + } + }; + useEffect(() => { if (!manager) { console.error("Manager object is null"); return; } setName(manager.name || ""); // Fallback to empty string - setRole("Manager"); + fetchRole(); setEmail(manager.email || ""); // Fallback to empty string }, [manager]); @@ -177,7 +207,7 @@ const Profile = () => { value={role} mode="outlined" style={styles.input} - editable={false} + onChangeText={(text) => setRole(text)} /> {/* Save Profile Button */} diff --git a/package-lock.json b/package-lock.json index 6401079b..7ac93871 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "expo-media-library": "~16.0.5", "expo-router": "~3.5.18", "expo-secure-store": "^13.0.2", + "expo-sharing": "~12.0.1", "expo-splash-screen": "~0.27.5", "expo-status-bar": "~1.12.1", "expo-system-ui": "~3.0.7", @@ -15538,6 +15539,15 @@ "expo": "*" } }, + "node_modules/expo-sharing": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-12.0.1.tgz", + "integrity": "sha512-wBT+WeXwapj/9NWuLJO01vi9bdlchYu/Q/xD8slL/Ls4vVYku8CPqzkTtDFcjLrjtlJqyeHsdQXwKLvORmBIew==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-splash-screen": { "version": "0.27.5", "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.27.5.tgz", diff --git a/package.json b/package.json index b0a7a12b..c5618fa3 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,8 @@ "react-native-toast-message": "^2.2.1", "react-native-video": "^6.7.0", "react-native-web": "~0.19.10", - "stream-chat-expo": "^5.41.2" + "stream-chat-expo": "^5.41.2", + "expo-sharing": "~12.0.1" }, "devDependencies": { "@babel/core": "^7.20.0", From 523cd990faf4f4ffc81e6ef3604297c6a5672c5b Mon Sep 17 00:00:00 2001 From: Manan Sharma <112090668+Manan-Sharma-5@users.noreply.github.com> Date: Sun, 5 Jan 2025 12:58:27 +0530 Subject: [PATCH 06/17] added underline in file attachment --- components/contract-card/UserResponse.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/components/contract-card/UserResponse.tsx b/components/contract-card/UserResponse.tsx index c9056248..e967576a 100644 --- a/components/contract-card/UserResponse.tsx +++ b/components/contract-card/UserResponse.tsx @@ -121,6 +121,7 @@ const UserResponse: FC = ({ key={index} style={{ fontSize: 16, + textDecorationLine: "underline", }} > {attachment.name} From 117bcb9ce5a7ad8a7729499ad3c9b9beb984d5a1 Mon Sep 17 00:00:00 2001 From: Manan Sharma <112090668+Manan-Sharma-5@users.noreply.github.com> Date: Mon, 6 Jan 2025 17:15:10 +0530 Subject: [PATCH 07/17] ui-feedback --- .../brand-profile/members-card/index.tsx | 73 +++---- .../card-components/ChipComponent.tsx | 3 +- components/contract-card/UserResponse.tsx | 14 +- components/contracts/ActionContainer.tsx | 155 +++++++-------- .../contracts/ContractDetailContent.tsx | 35 ++-- components/contracts/MemberContainer.tsx | 16 +- components/contracts/active.tsx | 12 +- components/contracts/past.tsx | 12 +- components/menu/index.tsx | 4 +- components/preferences/index.tsx | 8 +- components/proposals/Applications.tsx | 37 ++-- components/proposals/Invitations.tsx | 181 +++++++++--------- contexts/chat-context.provider.native.tsx | 26 +++ contexts/chat-context.provider.web.tsx | 7 + styles/menu/MenuItem.styles.tsx | 3 +- 15 files changed, 320 insertions(+), 266 deletions(-) diff --git a/components/brand-profile/members-card/index.tsx b/components/brand-profile/members-card/index.tsx index c85b3e56..90b6f6b3 100644 --- a/components/brand-profile/members-card/index.tsx +++ b/components/brand-profile/members-card/index.tsx @@ -6,15 +6,22 @@ import { imageUrl } from "@/utils/url"; import { faEllipsisH } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"; import { useTheme } from "@react-navigation/native"; -import { FC } from "react"; -import { Avatar } from "react-native-paper"; +import { FC, useState } from "react"; +import { Pressable } from "react-native"; +import { Avatar, Menu } from "react-native-paper"; interface MembersCardProps { manager: ManagerCard; + cardType: string; + action: () => void; } -const MembersCard: FC = ({ manager }) => { +const MembersCard: FC = ({ manager, cardType, action }) => { const theme = useTheme(); + const [menuVisible, setMenuVisible] = useState(false); // State to handle menu visibility + + const openMenu = () => setMenuVisible(true); + const closeMenu = () => setMenuVisible(false); if (!manager) { return null; @@ -41,41 +48,37 @@ const MembersCard: FC = ({ manager }) => { > - - - {manager.name} - - - {manager.email} - - + {manager.name} + {manager.email} - - {manager.status === 0 ? ( - + {manager.status === 0 && ( + Invite Sent + )} + + + + } + style={{ + backgroundColor: Colors(theme).background, + borderWidth: 0.3, + borderColor: Colors(theme).gray300, + }} + > + { + action(); + closeMenu(); }} - > - Invite Sent - - ) : null} - + title="Delete" + titleStyle={{ color: Colors(theme).text }} + /> + ); diff --git a/components/collaboration-card/card-components/ChipComponent.tsx b/components/collaboration-card/card-components/ChipComponent.tsx index e89564c7..28dd610d 100644 --- a/components/collaboration-card/card-components/ChipComponent.tsx +++ b/components/collaboration-card/card-components/ChipComponent.tsx @@ -18,7 +18,8 @@ const ChipCard: FC = ({ chipText, chipIcon }) => { = ({ @@ -86,13 +86,11 @@ const UserResponse: FC = ({ /> ))} - - {application?.message} - + {application?.message} @@ -114,7 +112,7 @@ const UserResponse: FC = ({ onPress={() => downloadAndSaveFile(attachment.url, attachment.name) } - style={{ flexDirection: "row", alignItems: "center", gap: 10 }} + style={{ flexDirection: "row", alignItems: "center", gap: 4 }} > = ({ {application?.answersFromInfluencer && @@ -152,6 +150,7 @@ const UserResponse: FC = ({ fontWeight: "bold", }} > + Q{") "} {influencerQuestions[answer.question]} = ({ fontSize: 16, }} > + A{") "} {answer.answer} diff --git a/components/contracts/ActionContainer.tsx b/components/contracts/ActionContainer.tsx index fe718b12..d73ea33d 100644 --- a/components/contracts/ActionContainer.tsx +++ b/components/contracts/ActionContainer.tsx @@ -108,87 +108,88 @@ const ActionContainer: FC = ({ - - {contract.status === 0 && ( - <> - - - - )} - {contract.status === 1 && ( - <> - - - - )} - {contract.status === 2 && !contract.feedbackFromBrand && ( - <> - - - )} - + {contract.status !== 3 && ( + + {contract.status === 0 && ( + <> + + + + )} + {contract.status === 1 && ( + <> + + + + )} + {contract.status === 2 && !contract.feedbackFromBrand && ( + <> + + + )} + + )} {contract.feedbackFromBrand && ( { ) : null} - No content available.

", }} - > - No content available.

", - }} - contentWidth={Dimensions.get("window").width} - defaultTextProps={{ - style: { - color: Colors(theme).text, - fontSize: 16, - lineHeight: 22, - }, - }} - /> -
+ contentWidth={Dimensions.get("window").width} + defaultTextProps={{ + style: { + color: Colors(theme).text, + fontSize: 16, + lineHeight: 22, + }, + }} + />
= ({ setShowModal, }) => { const theme = useTheme(); - const { fetchMembers } = useChatContext(); + const { fetchMembers, removeMemberFromChannel } = useChatContext(); const [updateMember, setUpdateMember] = React.useState(false); const [members, setMembers] = React.useState([]); @@ -95,7 +95,19 @@ const MemberContainer: FC = ({
} + renderItem={({ item }) => ( + { + await removeMemberFromChannel(channelId, item.managerId).then( + () => { + fetchMembersFromClient(); + } + ); + }} + /> + )} contentContainerStyle={{ gap: 10, }} diff --git a/components/contracts/active.tsx b/components/contracts/active.tsx index 23604bee..e68b5edb 100644 --- a/components/contracts/active.tsx +++ b/components/contracts/active.tsx @@ -14,7 +14,7 @@ import { doc, getDoc, } from "firebase/firestore"; -import { ActivityIndicator, FlatList } from "react-native"; +import { ActivityIndicator, FlatList, Pressable } from "react-native"; import { FirestoreDB } from "@/utils/firestore"; import { AuthApp } from "@/utils/auth"; import { RefreshControl } from "react-native"; @@ -167,10 +167,16 @@ const ActiveContracts = () => { data={filteredProposals} showsVerticalScrollIndicator={false} renderItem={({ item }) => ( - { router.push(`/contract-details/${item.streamChannelId}`); }} + style={{ + borderWidth: 0.3, + borderColor: Colors(theme).gray300, + borderRadius: 5, + overflow: "hidden", + }} > { } } /> - + )} keyExtractor={(item, index) => index.toString()} style={{ diff --git a/components/contracts/past.tsx b/components/contracts/past.tsx index 8520b249..4f362ff3 100644 --- a/components/contracts/past.tsx +++ b/components/contracts/past.tsx @@ -14,7 +14,7 @@ import { doc, getDoc, } from "firebase/firestore"; -import { ActivityIndicator, FlatList } from "react-native"; +import { ActivityIndicator, FlatList, Pressable } from "react-native"; import { FirestoreDB } from "@/utils/firestore"; import { AuthApp } from "@/utils/auth"; import { RefreshControl } from "react-native"; @@ -167,10 +167,16 @@ const PastContracts = () => { data={filteredProposals} showsVerticalScrollIndicator={false} renderItem={({ item }) => ( - { router.push(`/contract-details/${item.streamChannelId}`); }} + style={{ + borderWidth: 0.3, + borderColor: Colors(theme).gray300, + borderRadius: 5, + overflow: "hidden", + }} > { } } /> - + )} keyExtractor={(item, index) => index.toString()} style={{ diff --git a/components/menu/index.tsx b/components/menu/index.tsx index a1a10213..cf54d02a 100644 --- a/components/menu/index.tsx +++ b/components/menu/index.tsx @@ -41,9 +41,7 @@ const Menu = () => { {selectedBrand?.name} diff --git a/components/preferences/index.tsx b/components/preferences/index.tsx index efb7ba81..15c96842 100644 --- a/components/preferences/index.tsx +++ b/components/preferences/index.tsx @@ -75,7 +75,13 @@ const Preferences = () => { {/* */} } + renderItem={({ item }) => ( + {}} + /> + )} keyExtractor={(item) => item.managerId} contentContainerStyle={{ gap: 10, diff --git a/components/proposals/Applications.tsx b/components/proposals/Applications.tsx index d0afe3da..7c0feb05 100644 --- a/components/proposals/Applications.tsx +++ b/components/proposals/Applications.tsx @@ -157,7 +157,16 @@ const Applications = () => { data={filteredProposals} showsVerticalScrollIndicator={false} renderItem={({ item }) => ( - + {item.attachments && item.attachments?.length > 0 && ( { processRawAttachment(attachment) ) || [] } - dot={ - - } - activeDot={ - - } /> )} {item.status === "draft" && ( @@ -229,7 +214,7 @@ const Applications = () => { influencerCount={item.numberOfInfluencersNeeded} /> - + )} keyExtractor={(item, index) => index.toString()} style={{ diff --git a/components/proposals/Invitations.tsx b/components/proposals/Invitations.tsx index de6cba25..d0e76614 100644 --- a/components/proposals/Invitations.tsx +++ b/components/proposals/Invitations.tsx @@ -1,12 +1,12 @@ import BottomSheetActions from "@/components/BottomSheetActions"; import JobCard from "@/components/collaboration/CollaborationCard"; -import { View } from "@/components/theme/Themed"; +import { Text, View } from "@/components/theme/Themed"; import Colors from "@/constants/Colors"; import AppLayout from "@/layouts/app-layout"; import { useTheme } from "@react-navigation/native"; import { useEffect, useMemo, useState } from "react"; import { collection, getDocs, query, where } from "firebase/firestore"; -import { ActivityIndicator, FlatList } from "react-native"; +import { ActivityIndicator, FlatList, Pressable } from "react-native"; import { FirestoreDB } from "@/utils/firestore"; import { AuthApp } from "@/utils/auth"; import { RefreshControl } from "react-native"; @@ -20,6 +20,7 @@ import { DUMMY_INFLUENCER } from "@/constants/Influencer"; import Carousel from "@/shared-uis/components/carousel/carousel"; import { processRawAttachment } from "@/utils/attachments"; import { Card } from "react-native-paper"; +import { router } from "expo-router"; const Invitations = () => { const [isVisible, setIsVisible] = useState(false); @@ -39,6 +40,7 @@ const Invitations = () => { const user = AuthApp.currentUser; const [isLoading, setIsLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); const { xl } = useBreakpoints(); @@ -134,7 +136,7 @@ const Invitations = () => { flex: 1, }} > - {proposals.length === 0 ? ( + {filteredProposals.length === 0 ? ( { title="No Invitations yet" /> ) : ( - ( - - {item.attachments && item.attachments.length > 0 && ( - - processRawAttachment(attachment) - ) || [] - } - dot={ - - } - activeDot={ - - } + + ( + + {item.attachments && item.attachments?.length > 0 && ( + + processRawAttachment(attachment) + ) || [] + } + /> + )} + {item.status === "draft" && ( + + Draft + + )} + - )} - - - - )} - keyExtractor={(item, index) => index.toString()} - style={{ - flexGrow: 1, - paddingBottom: 16, - paddingHorizontal: 16, - paddingTop: 8, - }} - contentContainerStyle={{ - gap: 16, - paddingBottom: 24, - alignItems: xl ? "center" : "stretch", - }} - refreshControl={ - - } - horizontal={false} - numColumns={xl ? 2 : 1} // TODO: On fly can't be responsive - {...(xl && { - columnWrapperStyle: { + { + router.push(`/collaboration-details/${item.id}`); + }} + > + + + + )} + keyExtractor={(item, index) => index.toString()} + style={{ + flexGrow: 1, + paddingBottom: 16, + paddingHorizontal: 16, + paddingTop: 8, + }} + contentContainerStyle={{ gap: 16, - }, - })} - /> + paddingBottom: 24, + alignItems: xl ? "center" : "stretch", + }} + refreshControl={ + + } + horizontal={false} + numColumns={xl ? 2 : 1} // TODO: On fly can't be responsive + {...(xl && { + columnWrapperStyle: { + gap: 16, + }, + })} + /> +
)} {isVisible && ( void; sendSystemMessage: (channel: string, message: string) => void; fetchChannelCid: (channelId: string) => Promise; + removeMemberFromChannel: ( + channel: string, + member: string + ) => Promise; } const ChatContext = createContext({ @@ -34,6 +38,7 @@ const ChatContext = createContext({ addMemberToChannel: async () => {}, sendSystemMessage: async () => {}, fetchChannelCid: async () => "", + removeMemberFromChannel: async () => false, }); export const useChatContext = () => useContext(ChatContext); @@ -156,6 +161,26 @@ export const ChatContextProvider: React.FC = ({ channelToWatch.sendMessage(messageToSend); }; + const removeMemberFromChannel = async (channel: string, member: string) => { + try { + const channelToWatch = streamClient.channel("messaging", channel); + await channelToWatch + .removeMembers([member]) + .then(() => { + return true; + }) + .catch((error) => { + console.error(error); + return false; + }); + + return false; + } catch (error) { + console.error(error); + return false; + } + }; + const fetchChannelCid = async (channelId: string) => { const channel = streamClient.channel("messaging", channelId); await channel.watch(); @@ -173,6 +198,7 @@ export const ChatContextProvider: React.FC = ({ addMemberToChannel, sendSystemMessage, fetchChannelCid, + removeMemberFromChannel, }} > {children} diff --git a/contexts/chat-context.provider.web.tsx b/contexts/chat-context.provider.web.tsx index 7eee2b91..89e18bcf 100644 --- a/contexts/chat-context.provider.web.tsx +++ b/contexts/chat-context.provider.web.tsx @@ -11,6 +11,7 @@ interface ChatContextProps { addMemberToChannel: (channel: any, member: string) => void; sendSystemMessage: (channel: string, message: string) => void; fetchChannelCid: (channelId: string) => Promise; + removeMemberFromChannel: (channel: any, member: string) => Promise; } const ChatContext = createContext({ @@ -20,6 +21,7 @@ const ChatContext = createContext({ addMemberToChannel: async () => {}, sendSystemMessage: async () => {}, fetchChannelCid: async () => "", + removeMemberFromChannel: async () => false, }); export const useChatContext = () => useContext(ChatContext); @@ -60,6 +62,10 @@ export const ChatContextProvider: React.FC = ({ return ""; }; + const removeMemberFromChannel = async (channel: any, member: string) => { + return false; + }; + const connectUser = async () => { return null; }; @@ -73,6 +79,7 @@ export const ChatContextProvider: React.FC = ({ addMemberToChannel, sendSystemMessage, fetchChannelCid, + removeMemberFromChannel, }} > {children} diff --git a/styles/menu/MenuItem.styles.tsx b/styles/menu/MenuItem.styles.tsx index 5880309c..27a4f78b 100644 --- a/styles/menu/MenuItem.styles.tsx +++ b/styles/menu/MenuItem.styles.tsx @@ -28,12 +28,13 @@ const stylesFn = (theme: Theme) => flex: 1, paddingHorizontal: 20, paddingVertical: 10, + gap: 16, justifyContent: "space-between", }, topRow: { gap: 20, alignItems: "center", - paddingVertical: 20, + paddingTop: 20, }, middleRow: { flex: 1, From bcea307c8a0acaebce17558d002b9c181adf7118 Mon Sep 17 00:00:00 2001 From: Manan Sharma <112090668+Manan-Sharma-5@users.noreply.github.com> Date: Mon, 6 Jan 2025 20:02:42 +0530 Subject: [PATCH 08/17] added work --- components/preferences/index.tsx | 41 ++++++--- components/ui/modal/MembersModal.tsx | 133 ++++++++++++++++++++------- contexts/auth-context.provider.tsx | 21 +++-- styles/Members.tsx | 6 +- 4 files changed, 146 insertions(+), 55 deletions(-) diff --git a/components/preferences/index.tsx b/components/preferences/index.tsx index 15c96842..6e8d971b 100644 --- a/components/preferences/index.tsx +++ b/components/preferences/index.tsx @@ -1,10 +1,16 @@ -import { FlatList, ScrollView } from "react-native"; +import { + FlatList, + KeyboardAvoidingView, + Platform, + ScrollView, +} from "react-native"; import MembersCard from "../brand-profile/members-card"; import { View } from "../theme/Themed"; import Colors from "@/constants/Colors"; import { useTheme } from "@react-navigation/native"; import { useBrandContext } from "@/contexts/brand-context.provider"; import { collection, doc, getDoc, getDocs } from "firebase/firestore"; +import { RefreshControl } from "react-native"; import { FirestoreDB } from "@/utils/firestore"; import { IBrandsMembers } from "@/shared-libs/firestore/trendly-pro/models/brands"; import { useEffect, useState } from "react"; @@ -22,6 +28,7 @@ const Preferences = () => { const theme = useTheme(); const { selectedBrand } = useBrandContext(); const [members, setMembers] = useState([]); + const [refreshing, setRefreshing] = useState(false); const [showMemberModal, setShowMemberModal] = useState(false); const fetchMembers = async () => { if (!selectedBrand) return; @@ -60,6 +67,12 @@ const Preferences = () => { } }; + const handleRefresh = async () => { + setRefreshing(true); + await fetchMembers(); + setRefreshing(false); + }; + useEffect(() => { fetchMembers(); }, [selectedBrand]); @@ -86,6 +99,13 @@ const Preferences = () => { contentContainerStyle={{ gap: 10, }} + refreshControl={ + + } /> - - { - setShowMemberModal(false); - }} - refresh={fetchMembers} - theme={theme} - /> - + + { + setShowMemberModal(false); + }} + refresh={fetchMembers} + theme={theme} + /> ); }; diff --git a/components/ui/modal/MembersModal.tsx b/components/ui/modal/MembersModal.tsx index 61081ea3..60c3c4c4 100644 --- a/components/ui/modal/MembersModal.tsx +++ b/components/ui/modal/MembersModal.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Button, Chip, Modal, TextInput } from "react-native-paper"; +import { Button, Chip, Modal, Portal, TextInput } from "react-native-paper"; import { stylesFn } from "@/styles/Members"; import { useTheme } from "@react-navigation/native"; import { View, Text } from "@/components/theme/Themed"; @@ -7,8 +7,15 @@ import Colors from "@/constants/Colors"; import { useBrandContext } from "@/contexts/brand-context.provider"; import Toaster from "@/shared-uis/components/toaster/Toaster"; import axios from "axios"; -import { useAuthContext } from "@/contexts"; import { AuthApp } from "@/utils/auth"; +import { FirestoreDB } from "@/utils/firestore"; +import { doc, setDoc } from "firebase/firestore"; +import { + ActivityIndicator, + KeyboardAvoidingView, + Platform, + ScrollView, +} from "react-native"; interface MembersModalProps { visible: boolean; @@ -26,14 +33,19 @@ const MembersModal: React.FC = ({ const styles = stylesFn(theme); const [email, setEmail] = React.useState(""); + const [name, setName] = React.useState(""); + const [loading, setLoading] = React.useState(false); const { selectedBrand } = useBrandContext(); const addMember = async () => { if (!selectedBrand) return; - if (!email) { - Toaster.error("Please enter email"); + if (!email || !name) { + Toaster.error("Please enter all fields"); + return; } const user = await AuthApp.currentUser?.getIdToken(); + setLoading(true); + await axios .post( "https://be.trendly.pro/api/v1/brands/members", @@ -47,47 +59,104 @@ const MembersModal: React.FC = ({ }, } ) - .then((res) => { + .then(async (res) => { Toaster.success("User Invited Successfully"); + if (res.status === 200) { + try { + const userID = res.data.user.rawId; + const memberRef = doc(FirestoreDB, "managers", userID); + let userData = { + name: name, + email: email, + pushNotificationToken: { + ios: [], + android: [], + web: [], + }, + settings: { + theme: "light", + emailNotification: true, + pushNotification: true, + }, + }; + await setDoc(memberRef, userData); + } catch (e) { + console.error(e); + Toaster.error("Something wrong happened"); + } + } refresh(); handleModalClose(); }) .catch((e) => { Toaster.error("Something wrong happened"); console.error(e); + }) + .finally(() => { + setEmail(""); + setName(""); + setLoading(false); }); }; return ( - { - handleModalClose(); - }} - style={{ - justifyContent: "center", - alignItems: "center", - }} + - - Add Member - You can add members from here - - setEmail(value)} - style={styles.input} - autoFocus - /> + + { + handleModalClose(); + }} + style={{ + justifyContent: "center", + alignItems: "center", + }} + > + + Add Member + You can add members from here + + + setEmail(value)} + style={styles.input} + autoFocus + /> + setName(value)} + style={styles.input} + /> - - - - + + + + + + + ); }; diff --git a/contexts/auth-context.provider.tsx b/contexts/auth-context.provider.tsx index f6f681f1..80dc969c 100644 --- a/contexts/auth-context.provider.tsx +++ b/contexts/auth-context.provider.tsx @@ -113,11 +113,11 @@ export const AuthContextProvider: React.FC = ({ setSession(managerCredential.user.uid); - await fetch('https://be.trendly.pro/api/v1/chat/auth', { - method: 'POST', + await fetch("https://be.trendly.pro/api/v1/chat/auth", { + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${managerCredential.user.uid}`, + "Content-Type": "application/json", + Authorization: `Bearer ${managerCredential.user.uid}`, }, }); @@ -159,6 +159,7 @@ export const AuthContextProvider: React.FC = ({ emailNotification: true, pushNotification: true, }, + role: "manager", }; await setDoc(docRef, userData); @@ -170,13 +171,15 @@ export const AuthContextProvider: React.FC = ({ const checkVerification = async () => { await userCredential.user.reload(); if (userCredential.user.emailVerified) { - router.replace("/(onboarding)/onboarding-your-brand?firstBrand=true"); + router.replace( + "/(onboarding)/onboarding-your-brand?firstBrand=true" + ); setSession(userCredential.user.uid); - await fetch('https://be.trendly.pro/api/v1/chat/auth', { - method: 'POST', + await fetch("https://be.trendly.pro/api/v1/chat/auth", { + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${userCredential.user.uid}`, + "Content-Type": "application/json", + Authorization: `Bearer ${userCredential.user.uid}`, }, }); } else { diff --git a/styles/Members.tsx b/styles/Members.tsx index 28f5a722..eb7bf5fc 100644 --- a/styles/Members.tsx +++ b/styles/Members.tsx @@ -33,11 +33,11 @@ export const stylesFn = (theme: Theme) => justifyContent: "flex-end", }, modalContent: { - padding: 20, + padding: 16, + gap: 16, borderRadius: 10, backgroundColor: Colors(theme).background, width: 300, - gap: 12, }, modalInputContainer: { marginBottom: 10, @@ -57,7 +57,7 @@ export const stylesFn = (theme: Theme) => margin: 4, }, addButton: { - marginTop: 10, + alignItems: "center", }, noDataContainer: { flex: 1, From 977fd79aaf5abda86a724c349cf033fac1ec0885 Mon Sep 17 00:00:00 2001 From: Manan Sharma <112090668+Manan-Sharma-5@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:36:32 +0530 Subject: [PATCH 09/17] minor changes --- components/contracts/ActionContainer.tsx | 1 + components/contracts/AddMemberModal.tsx | 16 +++++++++++++++- components/contracts/active.tsx | 11 ++++++----- components/contracts/past.tsx | 11 ++++++----- components/menu/index.tsx | 1 + styles/menu/MenuItem.styles.tsx | 1 + 6 files changed, 30 insertions(+), 11 deletions(-) diff --git a/components/contracts/ActionContainer.tsx b/components/contracts/ActionContainer.tsx index d73ea33d..9d709db1 100644 --- a/components/contracts/ActionContainer.tsx +++ b/components/contracts/ActionContainer.tsx @@ -86,6 +86,7 @@ const ActionContainer: FC = ({ }).then(() => { Toaster.success("Contract ended"); refreshData(); + feedbackModalVisible(); }); }; diff --git a/components/contracts/AddMemberModal.tsx b/components/contracts/AddMemberModal.tsx index cd17bfe8..c27199fc 100644 --- a/components/contracts/AddMemberModal.tsx +++ b/components/contracts/AddMemberModal.tsx @@ -75,6 +75,7 @@ const AddMembersModal: FC = ({ ); members = members.filter((member) => { + if (member.status === 0) return false; return !membersAlreadyInContract.some((memberInContract) => { if (!memberInContract) return false; return memberInContract.email === member.email; @@ -172,7 +173,20 @@ const AddMembersModal: FC = ({ > Add your team members here - + {members.length !== 0 ? ( + + ) : ( + + No members to add + + )} ); diff --git a/components/contracts/active.tsx b/components/contracts/active.tsx index e68b5edb..b3f11820 100644 --- a/components/contracts/active.tsx +++ b/components/contracts/active.tsx @@ -76,7 +76,11 @@ const ActiveContracts = () => { } const contractsCol = collection(FirestoreDB, "contracts"); - const contractsSnapshot = await getDocs(contractsCol); + const contractQuery = query( + contractsCol, + where("brandId", "==", selectedBrand?.id) + ); + const contractsSnapshot = await getDocs(contractQuery); const contracts = await Promise.all( contractsSnapshot.docs.map(async (document) => { @@ -129,10 +133,7 @@ const ActiveContracts = () => { const filteredProposals = useMemo(() => { return proposals.filter((proposal) => { - return ( - proposal.status !== 3 && - proposal.collaborationData.brandId === selectedBrand?.id - ); + return proposal.status !== 3 && proposal.status !== 0; }); }, [proposals]); diff --git a/components/contracts/past.tsx b/components/contracts/past.tsx index 4f362ff3..31589f93 100644 --- a/components/contracts/past.tsx +++ b/components/contracts/past.tsx @@ -76,7 +76,11 @@ const PastContracts = () => { } const contractsCol = collection(FirestoreDB, "contracts"); - const contractsSnapshot = await getDocs(contractsCol); + const contractsQuery = query( + contractsCol, + where("brandId", "==", selectedBrand?.id) + ); + const contractsSnapshot = await getDocs(contractsQuery); const contracts = await Promise.all( contractsSnapshot.docs.map(async (document) => { @@ -129,10 +133,7 @@ const PastContracts = () => { const filteredProposals = useMemo(() => { return proposals.filter((proposal) => { - return ( - proposal.status === 3 && - proposal.collaborationData.brandId === selectedBrand?.id - ); + return proposal.status === 3; }); }, [proposals]); diff --git a/components/menu/index.tsx b/components/menu/index.tsx index cf54d02a..e5444b05 100644 --- a/components/menu/index.tsx +++ b/components/menu/index.tsx @@ -49,6 +49,7 @@ const Menu = () => { {selectedBrand?.profile?.about} diff --git a/styles/menu/MenuItem.styles.tsx b/styles/menu/MenuItem.styles.tsx index 27a4f78b..fd3db70c 100644 --- a/styles/menu/MenuItem.styles.tsx +++ b/styles/menu/MenuItem.styles.tsx @@ -23,6 +23,7 @@ const stylesFn = (theme: Theme) => brandName: { fontSize: 24, textAlign: "center", + color: Colors(theme).text, }, menuItemsContainer: { flex: 1, From e8f4328dd2bfd6e7d361c9291f384e0c813fc32e Mon Sep 17 00:00:00 2001 From: Devender Singh Rathore <59788555+dev-rathore@users.noreply.github.com> Date: Tue, 7 Jan 2025 18:11:42 +0530 Subject: [PATCH 10/17] Repurpose messaging screen (#53) * repurpose messaging screen * accept application changes * added phases * invitationId as influencerId * fixed signup signin * fix: bug on brand creation * minor changes * minor changes * minor changes * minor changes --- app/(auth)/_layout.tsx | 4 +- app/(auth)/create-new-account.tsx | 2 +- app/(auth)/forget-password.tsx | 82 ----------------- app/(auth)/forgot-password.tsx | 81 +++++++++++++++- app/(auth)/login.tsx | 4 +- app/(auth)/signup.tsx | 34 ------- app/(onboarding)/onboarding-get-started.tsx | 31 ++++--- app/(onboarding)/onboarding-your-brand.tsx | 18 ++-- app/_layout.tsx | 9 +- components/InfluencerCard.tsx | 12 --- components/channel/channel.tsx | 92 ++++++++++++++----- components/channel/chat-message-topbar.tsx | 42 +++++++++ .../channel/message-topbar/first-phase.tsx | 71 ++++++++++++++ .../channel/message-topbar/fourth-phase.tsx | 12 +++ .../channel/message-topbar/second-phase.tsx | 37 ++++++++ .../channel/message-topbar/third-phase.tsx | 87 ++++++++++++++++++ .../ApplicationsTabContent.tsx | 4 +- .../InvitationsTabContent.tsx | 12 ++- .../drawer-layout/DrawerMenuContent.tsx | 4 +- constants/ChatMessageTopbar.ts | 10 ++ contexts/auth-context.provider.tsx | 55 ++++++----- contexts/chat-context.provider.native.tsx | 10 +- contexts/chat-context.provider.web.tsx | 16 ++-- contexts/contract-context.provider.tsx | 57 ++++++++++++ contexts/index.ts | 6 ++ hooks/request/use-applications.tsx | 8 +- shared-libs | 2 +- shared-uis | 2 +- ....styles.tsx => forgot-password.styles.tsx} | 0 types/Contract.ts | 5 + utils/router.ts | 10 ++ 31 files changed, 593 insertions(+), 226 deletions(-) delete mode 100644 app/(auth)/forget-password.tsx delete mode 100644 app/(auth)/signup.tsx create mode 100644 components/channel/chat-message-topbar.tsx create mode 100644 components/channel/message-topbar/first-phase.tsx create mode 100644 components/channel/message-topbar/fourth-phase.tsx create mode 100644 components/channel/message-topbar/second-phase.tsx create mode 100644 components/channel/message-topbar/third-phase.tsx create mode 100644 constants/ChatMessageTopbar.ts create mode 100644 contexts/contract-context.provider.tsx rename styles/{forget-password.styles.tsx => forgot-password.styles.tsx} (100%) create mode 100644 types/Contract.ts create mode 100644 utils/router.ts diff --git a/app/(auth)/_layout.tsx b/app/(auth)/_layout.tsx index 29ba6ac0..6a6e1c8e 100644 --- a/app/(auth)/_layout.tsx +++ b/app/(auth)/_layout.tsx @@ -23,13 +23,13 @@ const AuthLayout = () => { }} /> { Already have an account?{" "} router.navigate("/(auth)/login")} + onPress={() => router.replace("/(auth)/login")} > Login diff --git a/app/(auth)/forget-password.tsx b/app/(auth)/forget-password.tsx deleted file mode 100644 index aaa572ea..00000000 --- a/app/(auth)/forget-password.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import fnStyles from "@/styles/forget-password.styles"; -import { useTheme } from "@react-navigation/native"; -import { useRouter } from "expo-router"; -import * as React from "react"; -import { View, Image, Text } from "react-native"; -import { TextInput, Button } from "react-native-paper"; -import { sendPasswordResetEmail } from "firebase/auth"; -import { AuthApp } from "@/utils/auth"; -import Toaster from "@/shared-uis/components/toaster/Toaster"; -import Colors from "@/constants/Colors"; - -const ForgotPasswordScreen = () => { - const [email, setEmail] = React.useState(""); - const router = useRouter(); - const theme = useTheme(); - const styles = fnStyles(theme); - - const handleResetPassword = () => { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - - if (!emailRegex.test(email)) { - alert("Please enter a valid email address"); - return; - } - - sendPasswordResetEmail(AuthApp, email) - .then(() => { - Toaster.success("Password reset email sent successfully"); - router.navigate("/(auth)/login"); - }) - .catch((error) => { - Toaster.error(error.message); - }); - }; - - return ( - - {/* Logo Section */} - - - {/* Title */} - Forgot Password - - {/* Email Input Field */} - - - {/* Reset Password Button */} - - - {/* Back to Login Prompt */} - - Remember your password?{" "} - router.navigate("/(auth)/login")} - > - Login - - - - ); -}; - -export default ForgotPasswordScreen; diff --git a/app/(auth)/forgot-password.tsx b/app/(auth)/forgot-password.tsx index dc373f47..11f53789 100644 --- a/app/(auth)/forgot-password.tsx +++ b/app/(auth)/forgot-password.tsx @@ -1,11 +1,82 @@ -import { Text, View } from "@/components/theme/Themed"; +import fnStyles from "@/styles/forgot-password.styles"; +import { useTheme } from "@react-navigation/native"; +import { useRouter } from "expo-router"; +import * as React from "react"; +import { View, Image, Text } from "react-native"; +import { TextInput, Button } from "react-native-paper"; +import { sendPasswordResetEmail } from "firebase/auth"; +import { AuthApp } from "@/utils/auth"; +import Toaster from "@/shared-uis/components/toaster/Toaster"; +import Colors from "@/constants/Colors"; + +const ForgotPasswordScreen = () => { + const [email, setEmail] = React.useState(""); + const router = useRouter(); + const theme = useTheme(); + const styles = fnStyles(theme); + + const handleResetPassword = () => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + if (!emailRegex.test(email)) { + alert("Please enter a valid email address"); + return; + } + + sendPasswordResetEmail(AuthApp, email) + .then(() => { + Toaster.success("Password reset email sent successfully"); + router.replace("/(auth)/login"); + }) + .catch((error) => { + Toaster.error(error.message); + }); + }; -const ForgotPassword = () => { return ( - - Forgot Password + + {/* Logo Section */} + + + {/* Title */} + Forgot Password + + {/* Email Input Field */} + + + {/* Reset Password Button */} + + + {/* Back to Login Prompt */} + + Remember your password?{" "} + router.replace("/(auth)/login")} + > + Login + + ); }; -export default ForgotPassword; +export default ForgotPasswordScreen; diff --git a/app/(auth)/login.tsx b/app/(auth)/login.tsx index 95117b54..9feac66d 100644 --- a/app/(auth)/login.tsx +++ b/app/(auth)/login.tsx @@ -68,7 +68,7 @@ const LoginScreen = () => { Don't have an account?{" "} router.navigate("/create-new-account")} + onPress={() => router.replace("/create-new-account")} > Sign Up @@ -76,7 +76,7 @@ const LoginScreen = () => { router.navigate("/forget-password")} + onPress={() => router.replace("/forgot-password")} > Forgot Password? diff --git a/app/(auth)/signup.tsx b/app/(auth)/signup.tsx deleted file mode 100644 index 03c42dd3..00000000 --- a/app/(auth)/signup.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Button } from "react-native"; -import { Text, View } from "@/components/theme/Themed"; -import { useAuthContext } from "@/contexts"; -import { DUMMY_MANAGER_CREDENTIALS } from "@/constants/Manager"; - -const Signup = () => { - const { - signUp, - } = useAuthContext(); - - const handleSignUp = () => { - signUp( - DUMMY_MANAGER_CREDENTIALS.name, - DUMMY_MANAGER_CREDENTIALS.email, - DUMMY_MANAGER_CREDENTIALS.password - ); - }; - - return ( - - Signup - diff --git a/app/(onboarding)/onboarding-your-brand.tsx b/app/(onboarding)/onboarding-your-brand.tsx index ff419d62..0b523d57 100644 --- a/app/(onboarding)/onboarding-your-brand.tsx +++ b/app/(onboarding)/onboarding-your-brand.tsx @@ -7,11 +7,10 @@ import { router, useLocalSearchParams } from "expo-router"; import { useTheme } from "@react-navigation/native"; import { addDoc, collection, doc, setDoc } from "firebase/firestore"; -import { AuthApp } from "@/utils/auth"; import { Brand } from "@/types/Brand"; import { FirestoreDB } from "@/utils/firestore"; import { useBrandContext } from "@/contexts/brand-context.provider"; -import { useFirebaseStorageContext } from "@/contexts"; +import { useAuthContext, useFirebaseStorageContext } from "@/contexts"; import AppLayout from "@/layouts/app-layout"; import BrandProfile from "@/components/brand-profile"; import fnStyles from "@/styles/onboarding/brand.styles"; @@ -48,9 +47,11 @@ const OnboardingScreen = () => { const { setSelectedBrand, } = useBrandContext(); + const { + manager: user, + } = useAuthContext(); const handleCreateBrand = async () => { - const user = AuthApp.currentUser; if (!user) { Toaster.error("User not found"); return; @@ -82,16 +83,19 @@ const OnboardingScreen = () => { "brands", docRef.id, "members", - user.uid + user.id ); await setDoc(managerRef, { - managerId: user.uid, + managerId: user.id, role, }).then(() => { - router.navigate({ + router.replace({ pathname: "/(onboarding)/onboarding-get-started", - params: { brandId: docRef.id }, + params: { + brandId: docRef.id, + firstBrand: firstBrand === "true" ? "true" : "false", + }, }); setSelectedBrand(brandData as Brand); }).catch((error) => { diff --git a/app/_layout.tsx b/app/_layout.tsx index 914aeda0..5ac5c84e 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -25,6 +25,7 @@ import { AWSContextProvider, CloudMessagingContextProvider, CollaborationContextProvider, + ContractContextProvider, FirebaseStorageContextProvider, NotificationContextProvider, useAuthContext, @@ -80,9 +81,11 @@ export default function RootLayout() { - - - + + + + + diff --git a/components/InfluencerCard.tsx b/components/InfluencerCard.tsx index 59bfc296..26fdb704 100644 --- a/components/InfluencerCard.tsx +++ b/components/InfluencerCard.tsx @@ -216,18 +216,6 @@ const InfluencerCard = (props: InfluencerCardPropsType) => { /> - - { - return router.push("/(main)/CollaborationHistory"); - }} - > - - {/* {influencer.jobsCompleted} Jobs completed ({influencer.successRate}{" "} - success rate) */} - 10 Jobs completed 100% success rate - - diff --git a/components/channel/channel.tsx b/components/channel/channel.tsx index 4565668c..d7a50425 100644 --- a/components/channel/channel.tsx +++ b/components/channel/channel.tsx @@ -1,34 +1,71 @@ -import AddMember from "@/components/channel/add-member"; -import { View } from "@/components/theme/Themed"; -import ScreenHeader from "@/components/ui/screen-header"; -import Colors from "@/constants/Colors"; -import { faPlus } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"; -import { useTheme } from "@react-navigation/native"; -import { useLocalSearchParams } from "expo-router"; import { useEffect, useState } from "react"; import { ActivityIndicator, Pressable } from "react-native"; import { Channel as ChannelType } from "stream-chat"; +import { useLocalSearchParams, useRouter } from "expo-router"; + +import { View } from "@/components/theme/Themed"; +import ScreenHeader from "@/components/ui/screen-header"; import { Channel, MessageInput, MessageList, useChatContext } from "stream-chat-expo"; +import ChatMessageTopbar from "./chat-message-topbar"; +import { IContracts } from "@/shared-libs/firestore/trendly-pro/models/contracts"; +import { useAuthContext, useContractContext } from "@/contexts"; +import { Avatar } from "react-native-paper"; +import Colors from "@/constants/Colors"; +import { imageUrl } from "@/utils/url"; +import { User } from "@/types/User"; +import { useTheme } from "@react-navigation/native"; const ChannelNative = () => { const [channel, setChannel] = useState(null); + const [contract, setContract] = useState(null); + const [influencer, setInfluencer] = useState(null); const { cid } = useLocalSearchParams<{ cid: string }>(); - const [modalVisible, setModalVisible] = useState(false); + const theme = useTheme(); const { client } = useChatContext(); + const { getContractById } = useContractContext(); + + const router = useRouter(); + + const { + getInfluencerById, + } = useAuthContext(); + + const fetchInfluencer = async ( + influencerId: string, + ) => { + const influencerData = await getInfluencerById(influencerId); + setInfluencer(influencerData); + } + + const fetchContract = async ( + contractId: string, + ) => { + const contractData = await getContractById(contractId); + setContract(contractData); + } useEffect(() => { const fetchChannel = async () => { const channels = await client.queryChannels({ cid }); setChannel(channels[0]); + + if (channels[0]?.data?.contractId) { + await fetchContract(channels[0]?.data?.contractId as string); + } }; fetchChannel(); }, [cid]); - if (!channel) { + useEffect(() => { + if (contract?.userId) { + fetchInfluencer(contract.userId); + } + }, [contract]); + + if (!channel || !contract) { return ( { ); } + const channelName = channel?.data?.name || ''; + const influencerName = influencer?.name ? ` w ${influencer.name}` : ''; + const title = channelName + influencerName || 'Chat'; + return ( setModalVisible(true)} + style={{ + marginRight: 8, + marginLeft: 12, + }} + onPress={() => { + router.push(`/contract-details/${contract.streamChannelId}`); + }} > - } /> + - ); } diff --git a/components/channel/chat-message-topbar.tsx b/components/channel/chat-message-topbar.tsx new file mode 100644 index 00000000..9c8e1f27 --- /dev/null +++ b/components/channel/chat-message-topbar.tsx @@ -0,0 +1,42 @@ +import FirstPhase from "./message-topbar/first-phase"; +import FourthPhase from "./message-topbar/fourth-phase"; +import SecondPhase from "./message-topbar/second-phase"; +import ThirdPhase from "./message-topbar/third-phase"; +import { Contract } from "@/types/Contract"; +import { useState } from "react"; + +interface ChatMessageTopbarProps { + contract: Contract; +} + +const ChatMessageTopbar: React.FC = ({ + contract, +}) => { + const [status, setStatus] = useState(contract.status || 0); + + if (status === 0) { + return ( + + ); + } else if (status === 1) { + return ; + } else if (status === 2) { + return ( + + ); + } else if (status === 3) { + return ; + } else { + return null; + } +} + +export default ChatMessageTopbar; diff --git a/components/channel/message-topbar/first-phase.tsx b/components/channel/message-topbar/first-phase.tsx new file mode 100644 index 00000000..7a615418 --- /dev/null +++ b/components/channel/message-topbar/first-phase.tsx @@ -0,0 +1,71 @@ +import { View } from "@/components/theme/Themed"; +import Button from "@/components/ui/button"; +import { CHAT_MESSAGE_TOPBAR_DESCRIPTION } from "@/constants/ChatMessageTopbar"; +import { useChatContext, useContractContext } from "@/contexts"; +import { IContracts } from "@/shared-libs/firestore/trendly-pro/models/contracts"; +import MessageTopbar from "@/shared-uis/components/chat-message-bar"; +import Toaster from "@/shared-uis/components/toaster/Toaster"; + +interface FirstPhaseProps { + contractId: string; + contract: IContracts; + setStatus: React.Dispatch>; +} + +const FirstPhase: React.FC = ({ + contractId, + contract, + setStatus, +}) => { + const { + updateContract, + } = useContractContext(); + + const { + sendSystemMessage, + } = useChatContext(); + + const startContract = async () => { + await updateContract(contractId, { + ...contract, + status: 1, + }).then(() => { + Toaster.success("Contract started"); + setStatus(1); + }); + } + + return ( + + + + + } + description={CHAT_MESSAGE_TOPBAR_DESCRIPTION.first} + /> + ); +} + +export default FirstPhase; diff --git a/components/channel/message-topbar/fourth-phase.tsx b/components/channel/message-topbar/fourth-phase.tsx new file mode 100644 index 00000000..803b605f --- /dev/null +++ b/components/channel/message-topbar/fourth-phase.tsx @@ -0,0 +1,12 @@ +import { CHAT_MESSAGE_TOPBAR_DESCRIPTION } from "@/constants/ChatMessageTopbar"; +import MessageTopbar from "@/shared-uis/components/chat-message-bar"; + +const FourthPhase = () => { + return ( + + ); +} + +export default FourthPhase; diff --git a/components/channel/message-topbar/second-phase.tsx b/components/channel/message-topbar/second-phase.tsx new file mode 100644 index 00000000..5fb99e08 --- /dev/null +++ b/components/channel/message-topbar/second-phase.tsx @@ -0,0 +1,37 @@ +import { Pressable } from "react-native"; +import { useTheme } from "@react-navigation/native"; +import { faClose } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"; + +import { CHAT_MESSAGE_TOPBAR_DESCRIPTION } from "@/constants/ChatMessageTopbar"; +import Colors from "@/constants/Colors"; +import MessageTopbar from "@/shared-uis/components/chat-message-bar"; + +interface SecondPhaseProps { + setStatus: React.Dispatch>; +} + +const SecondPhase: React.FC = ({ + setStatus, +}) => { + const theme = useTheme(); + + return ( + setStatus(-1)} + > + + + } + /> + ); +} + +export default SecondPhase; diff --git a/components/channel/message-topbar/third-phase.tsx b/components/channel/message-topbar/third-phase.tsx new file mode 100644 index 00000000..b62b5c86 --- /dev/null +++ b/components/channel/message-topbar/third-phase.tsx @@ -0,0 +1,87 @@ +import React, { useState } from "react"; +import { View } from "@/components/theme/Themed"; +import { useTheme } from "@react-navigation/native"; + +import Button from "@/components/ui/button"; +import { CHAT_MESSAGE_TOPBAR_DESCRIPTION } from "@/constants/ChatMessageTopbar"; +import MessageTopbar from "@/shared-uis/components/chat-message-bar"; +import FeedbackModal from "@/shared-uis/components/feedback-modal"; +import { IContracts } from "@/shared-libs/firestore/trendly-pro/models/contracts"; +import { useContractContext } from "@/contexts"; +import Toaster from "@/shared-uis/components/toaster/Toaster"; + +interface ThirdPhaseProps { + contract: IContracts; + contractId: string; + setStatus: React.Dispatch>; +} + +const ThirdPhase: React.FC = ({ + contract, + contractId, + setStatus, +}) => { + const [feedbackModalVisible, setFeedbackModalVisible] = useState(false); + const theme = useTheme(); + + const { + updateContract, + } = useContractContext(); + + const handleFeedbackSubmit = async ( + rating: number, + feedback: string, + ) => { + await updateContract(contractId, { + ...contract, + feedbackFromBrand: { + ratings: rating, + feedbackReview: feedback, + timeSubmitted: new Date().getTime(), + }, + }).then(() => { + Toaster.success("Feedback submitted successfully"); + setStatus(3); + }); + } + + return ( + <> + + + + } + description={CHAT_MESSAGE_TOPBAR_DESCRIPTION.third} + /> + { + handleFeedbackSubmit(rating, feedback).then(() => { + setFeedbackModalVisible(false); + }); + }} + isVisible={feedbackModalVisible} + setIsVisible={setFeedbackModalVisible} + theme={theme} + /> + + ); +} + +export default ThirdPhase; diff --git a/components/collaboration/collaboration-details/ApplicationsTabContent.tsx b/components/collaboration/collaboration-details/ApplicationsTabContent.tsx index 5a67ab22..601b738c 100644 --- a/components/collaboration/collaboration-details/ApplicationsTabContent.tsx +++ b/components/collaboration/collaboration-details/ApplicationsTabContent.tsx @@ -73,7 +73,9 @@ const ApplicationsTabContent = (props: any) => { loading, } = useApplications({ collaborationId: props.pageID, - data: props.collaboration, + data: { + collaboration: props.collaboration, + }, handleActionModalClose, influencerApplication: selectedInfluencerApplication as InfluencerApplication, }); diff --git a/components/collaboration/collaboration-details/InvitationsTabContent.tsx b/components/collaboration/collaboration-details/InvitationsTabContent.tsx index 091e288c..f8103ec2 100644 --- a/components/collaboration/collaboration-details/InvitationsTabContent.tsx +++ b/components/collaboration/collaboration-details/InvitationsTabContent.tsx @@ -13,7 +13,7 @@ import { import { useTheme } from "@react-navigation/native"; import { stylesFn } from "@/styles/collaboration-details/CollaborationDetails.styles"; import { FirestoreDB } from "@/utils/firestore"; -import { addDoc, collection } from "firebase/firestore"; +import { collection, doc, setDoc } from "firebase/firestore"; import { AuthApp } from "@/utils/auth"; import Toaster from "@/shared-uis/components/toaster/Toaster"; import { useInfluencers } from "@/hooks/request"; @@ -95,7 +95,7 @@ const InvitationsTabContent = (props: any) => { if (!selectedInfluencer) return; setIsInviting(true); - const invitationRef = collection( + const invitationColRef = collection( FirestoreDB, "collaborations", collaborationId, @@ -110,7 +110,9 @@ const InvitationsTabContent = (props: any) => { message: message, }; - await addDoc(invitationRef, invitationPayload).then(() => { + // Invitation Id as influencer id + const invitationDocRef = doc(invitationColRef, selectedInfluencer.id); + await setDoc(invitationDocRef, invitationPayload).then(() => { setIsInvitationModalVisible(false); Toaster.success("Invitation sent successfully"); }); @@ -223,7 +225,9 @@ const InvitationsTabContent = (props: any) => {