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/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 && (
+
+ )
+ }
+
+
+ Reject Application
+
+
+ Accept Application
+
+
+ }
+ />
+
+ );
+}
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) => {
setIsMessageModalVisible(false)}
+ onPress={() => setIsInvitationModalVisible(false)}
>
Cancel
@@ -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 (
+ {
+ router.push({
+ pathname: "/(modal)/create-collaboration",
+ });
+ }}
+ >
+
+ Create Collaboration
+
);
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
+ )}
+
- ) : 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 && (
- <>
- {
- sendSystemMessage(
- contract.streamChannelId,
- "Please revise the quote"
- );
- Toaster.success("Message sent to influencer");
- }}
- >
- Ask To Revise Quote
-
-
- Start Contract
-
- >
- )}
- {contract.status === 1 && (
- <>
-
- End Contract
-
- {
- const channelCid = await fetchChannelCid(
- contract.streamChannelId
- );
- router.navigate(`/channel/${channelCid}`);
- }}
- >
- Go to Messages
-
- >
- )}
- {contract.status === 2 && !contract.feedbackFromBrand && (
- <>
-
- Give Feedback
-
- >
- )}
-
+ {contract.status !== 3 && (
+
+ {contract.status === 0 && (
+ <>
+ {
+ sendSystemMessage(
+ contract.streamChannelId,
+ "Please revise the quote"
+ );
+ Toaster.success("Message sent to influencer");
+ }}
+ >
+ Ask To Revise Quote
+
+
+ Start Contract
+
+ >
+ )}
+ {contract.status === 1 && (
+ <>
+
+ End Contract
+
+ {
+ const channelCid = await fetchChannelCid(
+ contract.streamChannelId
+ );
+ router.navigate(`/channel/${channelCid}`);
+ }}
+ >
+ Go to Messages
+
+ >
+ )}
+ {contract.status === 2 && !contract.feedbackFromBrand && (
+ <>
+
+ Give Feedback
+
+ >
+ )}
+
+ )}
{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={
+
+ }
/>
{
>
Add Member
-
- {
- 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}
+ />
-
- Add Member
-
-
-
-
+
+ {loading ? : "Add Member"}
+
+
+
+
+
+
+
);
};
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 */}
- {
- handleResetPassword();
- }}
- >
- Reset Password
-
-
- {/* 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 */}
+ {
+ handleResetPassword();
+ }}
+ >
+ Reset Password
+
+
+ {/* 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
-
-
- );
-};
-
-export default Signup;
diff --git a/app/(onboarding)/onboarding-get-started.tsx b/app/(onboarding)/onboarding-get-started.tsx
index 406e9910..dcf85163 100644
--- a/app/(onboarding)/onboarding-get-started.tsx
+++ b/app/(onboarding)/onboarding-get-started.tsx
@@ -12,12 +12,13 @@ import { doc, getDoc, setDoc } from "firebase/firestore";
import { FirestoreDB } from "@/utils/firestore";
import { useBrandContext } from "@/contexts/brand-context.provider";
import { Brand } from "@/types/Brand";
+import Toaster from "@/shared-uis/components/toaster/Toaster";
const GetStartedScreen = () => {
const [hearAboutUs, setHearAboutUs] = useState("");
const [useFor, setUseFor] = useState("");
const [volumeOfCollaboration, setVolumeOfCollaboration] = useState("");
- const { firebaseSignIn } = useAuthContext();
+ const { setSession } = useAuthContext();
const theme = useTheme();
const styles = fnStyles(theme);
@@ -34,22 +35,27 @@ const GetStartedScreen = () => {
const openMenu3 = () => setVisible3(true);
const closeMenu3 = () => setVisible3(false);
- const handleSignUp = () => {
- firebaseSignIn(AuthApp.currentUser?.uid || "");
- };
-
- const params = useLocalSearchParams();
+ const {
+ brandId,
+ firstBrand,
+ } = useLocalSearchParams();
const {
setSelectedBrand,
} = useBrandContext();
+ const handleSkip = () => {
+ setSession(AuthApp.currentUser?.uid || "");
+ router.replace("/explore-influencers");
+ Toaster.success(firstBrand === "true" ? "Signed In Successfully!" : "Brand Created Successfully!");
+ };
+
const handleSubmit = async () => {
try {
- const { brandId } = params;
if (brandId) {
//@ts-ignore
const brandRef = doc(FirestoreDB, "brands", brandId);
+
await setDoc(
brandRef,
{
@@ -60,7 +66,7 @@ const GetStartedScreen = () => {
},
},
{ merge: true }
- );
+ )
const brandData = await getDoc(brandRef);
@@ -69,7 +75,8 @@ const GetStartedScreen = () => {
id: brandRef.id,
});
- router.push("/(main)/explore-influencers");
+ router.replace("/explore-influencers");
+ Toaster.success(firstBrand === "true" ? "Signed In Successfully!" : "Brand Created Successfully!");
}
} catch (error) {
console.error(error);
@@ -85,7 +92,7 @@ const GetStartedScreen = () => {
{
- handleSignUp();
+ handleSkip();
}}
>
Skip
@@ -212,13 +219,13 @@ const GetStartedScreen = () => {
{
- handleSignUp();
+ handleSubmit();
}}
style={{
width: "100%",
}}
>
- Take me in
+ {firstBrand === "true" ? "Take me in" : "Submit"}
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 (
+
+
+ Start Collaboration
+
+ {
+ sendSystemMessage(contractId, "Brand has requested to revise the quote.");
+ }}
+ >
+ Ask to Revise Quote
+
+
+ }
+ 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 (
+ <>
+
+ setFeedbackModalVisible(true)}
+ >
+ Give Feedback
+
+
+ }
+ 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) => {
{
+ handleCollaborationInvite();
+ }}
loading={isInviting}
>
Send Invitation
diff --git a/components/drawer-layout/DrawerMenuContent.tsx b/components/drawer-layout/DrawerMenuContent.tsx
index 92a7a075..81aee615 100644
--- a/components/drawer-layout/DrawerMenuContent.tsx
+++ b/components/drawer-layout/DrawerMenuContent.tsx
@@ -241,7 +241,9 @@ const DrawerMenuContent: React.FC = () => {
icon={faPlus}
showChevron={false}
onPress={() => {
- router.push("/(onboarding)/onboarding-your-brand");
+ router.push({
+ pathname: "/(onboarding)/onboarding-your-brand",
+ });
navigation.dispatch(DrawerActions.closeDrawer());
}}
title="Create New Brand"
diff --git a/constants/ChatMessageTopbar.ts b/constants/ChatMessageTopbar.ts
new file mode 100644
index 00000000..9fa0c3a4
--- /dev/null
+++ b/constants/ChatMessageTopbar.ts
@@ -0,0 +1,10 @@
+export const CHAT_MESSAGE_TOPBAR_DESCRIPTION = {
+ first:
+ "Please make sure to use this chat to first understand the influencer. Post that, you can start your collaboration here.",
+ second:
+ "Once the task is done, please make sure you end the collaboration from the collaboration details screen.",
+ third:
+ "Thanks for working through Trendly. We would love you to give feedback about this client.",
+ fourth:
+ "Your collaboration is over. However you can connect talk to the brand and be part of this group as long as you want.",
+};
diff --git a/contexts/auth-context.provider.tsx b/contexts/auth-context.provider.tsx
index 80dc969c..135457fb 100644
--- a/contexts/auth-context.provider.tsx
+++ b/contexts/auth-context.provider.tsx
@@ -27,13 +27,15 @@ import {
} from "firebase/auth";
import { analyticsLogEvent } from "@/utils/analytics";
import { checkTestUsers } from "@/utils/test-users";
+import { resetAndNavigate } from "@/utils/router";
+import { User } from "@/types/User";
interface AuthContextProps {
- firebaseSignIn: (token: string) => void;
- firebaseSignUp: (token: string) => void;
getManager: (managerId: string) => Promise;
+ getInfluencerById: (influencerId: string) => Promise;
isLoading: boolean;
session?: string | null;
+ setSession: (value: string | null) => void;
signIn: (email: string, password: string) => void;
signOutManager: () => void;
signUp: (name: string, email: string, password: string) => void;
@@ -45,10 +47,10 @@ interface AuthContextProps {
}
const AuthContext = createContext({
- firebaseSignIn: (token: string) => null,
- firebaseSignUp: (token: string) => null,
getManager: () => Promise.resolve(null),
+ getInfluencerById: () => Promise.resolve(null),
isLoading: false,
+ setSession: () => null,
session: null,
signIn: (email: string, password: string) => null,
signOutManager: () => null,
@@ -141,6 +143,7 @@ export const AuthContextProvider: React.FC = ({
Toaster.error("Please fill in all fields.");
return;
}
+
await createUserWithEmailAndPassword(AuthApp, email, password)
.then(async (userCredential) => {
const colRef = collection(FirestoreDB, "managers");
@@ -171,12 +174,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"
- );
setSession(userCredential.user.uid);
- await fetch("https://be.trendly.pro/api/v1/chat/auth", {
- method: "POST",
+ resetAndNavigate({
+ pathname: "/(onboarding)/onboarding-your-brand",
+ params: {
+ firstBrand: "true",
+ },
+ });
+ await fetch('https://be.trendly.pro/api/v1/chat/auth', {
+ method: 'POST',
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${userCredential.user.uid}`,
@@ -207,18 +213,6 @@ export const AuthContextProvider: React.FC = ({
});
};
- const firebaseSignIn = async (token: string) => {
- setSession(token);
- const user = await getManager(token);
- router.replace("/explore-influencers");
- Toaster.success("Signed In Successfully!");
- };
-
- const firebaseSignUp = async (token: string) => {
- setSession(token);
- Toaster.success("Signed Up Successfully!");
- };
-
const signOutManager = () => {
signOut(AuthApp)
.then(() => {
@@ -254,6 +248,21 @@ export const AuthContextProvider: React.FC = ({
return null;
};
+ const getInfluencerById = async (influencerId: string): Promise => {
+ const userRef = doc(FirestoreDB, "users", influencerId);
+ const userSnap = await getDoc(userRef);
+
+ if (userSnap.exists()) {
+ const userData = {
+ ...(userSnap.data() as User),
+ id: userSnap.id as string,
+ };
+ return userData;
+ }
+
+ return null;
+ };
+
const updateManager = async (
managerId: string,
manager: Partial
@@ -268,11 +277,11 @@ export const AuthContextProvider: React.FC = ({
return (
({
createGroupWithMembers: async () => Promise.resolve({} as Channel),
- connectUser: async () => {},
- fetchMembers: async () => {},
- addMemberToChannel: async () => {},
- sendSystemMessage: async () => {},
+ connectUser: async () => { },
+ fetchMembers: async () => { },
+ addMemberToChannel: async () => { },
+ sendSystemMessage: async () => { },
fetchChannelCid: async () => "",
removeMemberFromChannel: async () => false,
});
@@ -139,7 +139,7 @@ export const ChatContextProvider: React.FC = ({
const channelToWatch = streamClient.channel("messaging", channel);
await channelToWatch
.addMembers([member])
- .then(() => {})
+ .then(() => { })
.catch((error) => {
console.error(error);
});
diff --git a/contexts/chat-context.provider.web.tsx b/contexts/chat-context.provider.web.tsx
index 89e18bcf..8261630e 100644
--- a/contexts/chat-context.provider.web.tsx
+++ b/contexts/chat-context.provider.web.tsx
@@ -15,11 +15,11 @@ interface ChatContextProps {
}
const ChatContext = createContext({
- createGroupWithMembers: async () => {},
- connectUser: async () => {},
- fetchMembers: async () => {},
- addMemberToChannel: async () => {},
- sendSystemMessage: async () => {},
+ createGroupWithMembers: async () => { },
+ connectUser: async () => { },
+ fetchMembers: async () => { },
+ addMemberToChannel: async () => { },
+ sendSystemMessage: async () => { },
fetchChannelCid: async () => "",
removeMemberFromChannel: async () => false,
});
@@ -52,11 +52,11 @@ export const ChatContextProvider: React.FC = ({
return data.channel;
};
- const fetchMembers = async (channel: any): Promise => {};
+ const fetchMembers = async (channel: any): Promise => { };
- const addMemberToChannel = async (channel: any, member: string) => {};
+ const addMemberToChannel = async (channel: any, member: string) => { };
- const sendSystemMessage = async (channel: string, message: string) => {};
+ const sendSystemMessage = async (channel: string, message: string) => { };
const fetchChannelCid = async (channelId: string): Promise => {
return "";
diff --git a/contexts/contract-context.provider.tsx b/contexts/contract-context.provider.tsx
new file mode 100644
index 00000000..6ec77b8b
--- /dev/null
+++ b/contexts/contract-context.provider.tsx
@@ -0,0 +1,57 @@
+import {
+ useContext,
+ createContext,
+ type PropsWithChildren,
+} from "react";
+import { updateDoc, doc, getDoc } from "firebase/firestore";
+import { FirestoreDB } from "@/utils/firestore";
+
+import { IContracts } from "@/shared-libs/firestore/trendly-pro/models/contracts";
+
+interface ContractContextProps {
+ getContractById: (contractId: string) => Promise;
+ updateContract: (
+ contractId: string,
+ contract: Partial
+ ) => Promise;
+}
+
+const ContractContext = createContext({
+ getContractById: async () => ({} as IContracts),
+ updateContract: () => Promise.resolve(),
+});
+
+export const useContractContext = () => useContext(ContractContext);
+
+export const ContractContextProvider: React.FC = ({
+ children,
+}) => {
+ const getContractById = async (contractId: string) => {
+ const contractRef = doc(FirestoreDB, "contracts", contractId);
+ const contractSnap = await getDoc(contractRef);
+
+ return contractSnap.data() as IContracts
+ }
+
+ const updateContract = async (
+ contractId: string,
+ contract: Partial,
+ ) => {
+ const contractRef = doc(FirestoreDB, "contracts", contractId);
+
+ await updateDoc(contractRef, {
+ ...contract,
+ });
+ }
+
+ return (
+
+ {children}
+
+ );
+};
\ No newline at end of file
diff --git a/contexts/index.ts b/contexts/index.ts
index cedc154e..df0dcd26 100644
--- a/contexts/index.ts
+++ b/contexts/index.ts
@@ -17,6 +17,10 @@ import {
useCollaborationContext,
} from "./collaboration-context.provider";
import { AWSContextProvider, useAWSContext } from "./aws-context.provider";
+import {
+ ContractContextProvider,
+ useContractContext,
+} from "./contract-context.provider";
export {
AuthContextProvider,
@@ -24,6 +28,7 @@ export {
ChatContextProvider,
CloudMessagingContextProvider,
CollaborationContextProvider,
+ ContractContextProvider,
FirebaseStorageContextProvider,
NotificationContextProvider,
useAuthContext,
@@ -31,6 +36,7 @@ export {
useChatContext,
useCloudMessagingContext,
useCollaborationContext,
+ useContractContext,
useFirebaseStorageContext,
useNotificationContext,
};
diff --git a/hooks/request/use-applications.tsx b/hooks/request/use-applications.tsx
index 683a9d8d..7e6b30aa 100644
--- a/hooks/request/use-applications.tsx
+++ b/hooks/request/use-applications.tsx
@@ -10,7 +10,13 @@ import { User } from "@/types/User";
interface UseApplicationsProps {
collaborationId: string;
- data: any;
+ data: {
+ collaboration: {
+ id: string;
+ name: string;
+ questionsToInfluencers: string[];
+ },
+ };
handleActionModalClose: () => void;
influencerApplication: InfluencerApplication;
}
diff --git a/shared-libs b/shared-libs
index 5cbb5197..6d95458e 160000
--- a/shared-libs
+++ b/shared-libs
@@ -1 +1 @@
-Subproject commit 5cbb5197b939f1bb52a34295499b95fc7d792d1a
+Subproject commit 6d95458ecfe891cabf153b5a162080b8ff711fdd
diff --git a/shared-uis b/shared-uis
index 0bde09fd..f7719b1e 160000
--- a/shared-uis
+++ b/shared-uis
@@ -1 +1 @@
-Subproject commit 0bde09fd1a00bd6135b24ea6f44ae8477b3712fe
+Subproject commit f7719b1e63141841d9eabfc029a7eefedc5ae669
diff --git a/styles/forget-password.styles.tsx b/styles/forgot-password.styles.tsx
similarity index 100%
rename from styles/forget-password.styles.tsx
rename to styles/forgot-password.styles.tsx
diff --git a/types/Contract.ts b/types/Contract.ts
new file mode 100644
index 00000000..ec6c83b3
--- /dev/null
+++ b/types/Contract.ts
@@ -0,0 +1,5 @@
+import { IContracts } from "@/shared-libs/firestore/trendly-pro/models/contracts";
+
+export interface Contract extends IContracts {
+ id: string;
+}
diff --git a/utils/router.ts b/utils/router.ts
new file mode 100644
index 00000000..3a12b18b
--- /dev/null
+++ b/utils/router.ts
@@ -0,0 +1,10 @@
+import { router } from "expo-router";
+import { ExpoRouter } from "expo-router/types/expo-router";
+
+export const resetAndNavigate = (newPath: ExpoRouter.Href) => {
+ if (router.canGoBack()) {
+ router.dismissAll();
+ }
+
+ router.replace(newPath);
+};
From da85095e0fe476e2d8c48ddbbc258296f28c7746 Mon Sep 17 00:00:00 2001
From: Manan Sharma <112090668+Manan-Sharma-5@users.noreply.github.com>
Date: Tue, 7 Jan 2025 18:27:15 +0530
Subject: [PATCH 11/17] pushed a minor change
---
components/proposals/Applications.tsx | 54 +++++++++++++--------------
shared-libs | 2 +-
shared-uis | 2 +-
3 files changed, 29 insertions(+), 29 deletions(-)
diff --git a/components/proposals/Applications.tsx b/components/proposals/Applications.tsx
index 7c0feb05..0dcd7c79 100644
--- a/components/proposals/Applications.tsx
+++ b/components/proposals/Applications.tsx
@@ -179,35 +179,35 @@ const Applications = () => {
}
/>
)}
- {item.status === "draft" && (
-
- Draft
-
- )}
-
{
- router.push(`/collaboration-details/${item.id}`);
- }}
+ onPress={() =>
+ router.push(`/collaboration-details/${item.id}`)
+ }
>
+ {item.status === "draft" && (
+
+ Draft
+
+ )}
+
Date: Tue, 7 Jan 2025 20:33:19 +0530
Subject: [PATCH 12/17] fix: accept application (#56)
* fix: accept application
* fix: managerId
---
components/BottomSheetActions.tsx | 6 ++++--
.../channel/message-topbar/third-phase.tsx | 6 +++++-
.../ApplicationsTabContent.tsx | 14 ++++++++------
.../InvitationsTabContent.tsx | 1 +
contexts/chat-context.provider.native.tsx | 9 ++++++---
contexts/chat-context.provider.web.tsx | 9 ++++++---
hooks/request/use-applications.tsx | 16 ++++++++++------
7 files changed, 40 insertions(+), 21 deletions(-)
diff --git a/components/BottomSheetActions.tsx b/components/BottomSheetActions.tsx
index 5fa39660..9bfc3d6b 100644
--- a/components/BottomSheetActions.tsx
+++ b/components/BottomSheetActions.tsx
@@ -64,9 +64,11 @@ const BottomSheetActions = ({
await updateDoc(applicationRef, {
status: "accepted",
}).then(() => {
- createGroupWithMembers(data.collaboration.name, [
+ createGroupWithMembers(
+ data.collaboration.name,
cardId.influencerID,
- ]).then((channel) => {
+ cardId.collaborationID
+ ).then((channel) => {
connectUser();
createNotification(
diff --git a/components/channel/message-topbar/third-phase.tsx b/components/channel/message-topbar/third-phase.tsx
index b62b5c86..26c267c7 100644
--- a/components/channel/message-topbar/third-phase.tsx
+++ b/components/channel/message-topbar/third-phase.tsx
@@ -7,7 +7,7 @@ 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 { useAuthContext, useContractContext } from "@/contexts";
import Toaster from "@/shared-uis/components/toaster/Toaster";
interface ThirdPhaseProps {
@@ -27,6 +27,9 @@ const ThirdPhase: React.FC = ({
const {
updateContract,
} = useContractContext();
+ const {
+ manager,
+ } = useAuthContext();
const handleFeedbackSubmit = async (
rating: number,
@@ -38,6 +41,7 @@ const ThirdPhase: React.FC = ({
ratings: rating,
feedbackReview: feedback,
timeSubmitted: new Date().getTime(),
+ managerId: manager?.id,
},
}).then(() => {
Toaster.success("Feedback submitted successfully");
diff --git a/components/collaboration/collaboration-details/ApplicationsTabContent.tsx b/components/collaboration/collaboration-details/ApplicationsTabContent.tsx
index 601b738c..2ce69c13 100644
--- a/components/collaboration/collaboration-details/ApplicationsTabContent.tsx
+++ b/components/collaboration/collaboration-details/ApplicationsTabContent.tsx
@@ -77,7 +77,6 @@ const ApplicationsTabContent = (props: any) => {
collaboration: props.collaboration,
},
handleActionModalClose,
- influencerApplication: selectedInfluencerApplication as InfluencerApplication,
});
useEffect(() => {
@@ -115,7 +114,10 @@ const ApplicationsTabContent = (props: any) => {
data={influencers}
renderItem={({ item }) => (
{
+ setSelectedInfluencerApplication(item);
+ handleAcceptApplication(item);
+ }}
data={item}
headerLeftAction={() => {
setSelectedInfluencerApplication(item);
@@ -162,13 +164,13 @@ const ApplicationsTabContent = (props: any) => {
{
- handleAcceptApplication();
+ handleAcceptApplication(selectedInfluencerApplication as InfluencerApplication);
}}
/>
{
- handleRejectApplication();
+ handleRejectApplication(selectedInfluencerApplication as InfluencerApplication);
}}
/>
@@ -197,10 +199,10 @@ const ApplicationsTabContent = (props: any) => {
{
- handleRejectApplication();
+ handleRejectApplication(selectedInfluencerApplication as InfluencerApplication);
}}
onAccept={() => {
- handleAcceptApplication();
+ handleAcceptApplication(selectedInfluencerApplication as InfluencerApplication);
}}
questions={selectedInfluencerApplication?.application.answersFromInfluencer.map((answer) => ({
question: collaborationQuestions[answer.question],
diff --git a/components/collaboration/collaboration-details/InvitationsTabContent.tsx b/components/collaboration/collaboration-details/InvitationsTabContent.tsx
index f8103ec2..80039f8e 100644
--- a/components/collaboration/collaboration-details/InvitationsTabContent.tsx
+++ b/components/collaboration/collaboration-details/InvitationsTabContent.tsx
@@ -174,6 +174,7 @@ const InvitationsTabContent = (props: any) => {
setIsActionModalVisible(true);
}}
inviteInfluencer={() => {
+ setSelectedInfluencer(item);
setIsInvitationModalVisible(true);
}}
/>
diff --git a/contexts/chat-context.provider.native.tsx b/contexts/chat-context.provider.native.tsx
index e8c54b17..3ca3e0cc 100644
--- a/contexts/chat-context.provider.native.tsx
+++ b/contexts/chat-context.provider.native.tsx
@@ -18,7 +18,8 @@ const streamClient = StreamChat.getInstance(
interface ChatContextProps {
createGroupWithMembers: (
groupName: string,
- members: string[]
+ userId: string,
+ collaborationId: string,
) => Promise;
connectUser: () => void;
fetchMembers: (channel: string) => Promise;
@@ -107,7 +108,8 @@ export const ChatContextProvider: React.FC = ({
const createGroupWithMembers = async (
groupName: string,
- members: string[]
+ userId: string,
+ collaborationId: string,
): Promise => {
const response = await fetch("https://be.trendly.pro/api/v1/chat/channel", {
method: "POST",
@@ -117,7 +119,8 @@ export const ChatContextProvider: React.FC = ({
},
body: JSON.stringify({
name: groupName,
- userIds: members,
+ userId,
+ collaborationId,
}),
});
diff --git a/contexts/chat-context.provider.web.tsx b/contexts/chat-context.provider.web.tsx
index 8261630e..57dfecfd 100644
--- a/contexts/chat-context.provider.web.tsx
+++ b/contexts/chat-context.provider.web.tsx
@@ -4,7 +4,8 @@ import { useAuthContext } from "./auth-context.provider";
interface ChatContextProps {
createGroupWithMembers: (
groupName: string,
- members: string[]
+ userId: string,
+ collaborationId: string,
) => Promise;
connectUser: () => void;
fetchMembers: (channel: any) => Promise;
@@ -33,7 +34,8 @@ export const ChatContextProvider: React.FC = ({
const createGroupWithMembers = async (
groupName: string,
- members: string[]
+ userId: string,
+ collaborationId: string,
): Promise => {
const response = await fetch("https://be.trendly.pro/api/v1/chat/channel", {
method: "POST",
@@ -43,7 +45,8 @@ export const ChatContextProvider: React.FC = ({
},
body: JSON.stringify({
name: groupName,
- userIds: members,
+ userId,
+ collaborationId,
}),
});
diff --git a/hooks/request/use-applications.tsx b/hooks/request/use-applications.tsx
index 7e6b30aa..1841852b 100644
--- a/hooks/request/use-applications.tsx
+++ b/hooks/request/use-applications.tsx
@@ -18,14 +18,12 @@ interface UseApplicationsProps {
},
};
handleActionModalClose: () => void;
- influencerApplication: InfluencerApplication;
}
const useApplications = ({
collaborationId,
data,
handleActionModalClose,
- influencerApplication,
}: UseApplicationsProps) => {
const { createGroupWithMembers, connectUser } = useChatContext();
const { createNotification } = useNotificationContext();
@@ -77,7 +75,9 @@ const useApplications = ({
}
};
- const handleAcceptApplication = async () => {
+ const handleAcceptApplication = async (
+ influencerApplication: InfluencerApplication,
+ ) => {
try {
if (!influencerApplication.application) return;
@@ -91,9 +91,11 @@ const useApplications = ({
await updateDoc(applicationRef, {
status: "accepted",
}).then(() => {
- createGroupWithMembers(data.collaboration.name, [
+ createGroupWithMembers(
+ data.collaboration.name,
influencerApplication.application.userId,
- ]).then((channel) => {
+ data.collaboration.id,
+ ).then((channel) => {
connectUser();
createNotification(
@@ -125,7 +127,9 @@ const useApplications = ({
}
};
- const handleRejectApplication = async () => {
+ const handleRejectApplication = async (
+ influencerApplication: InfluencerApplication,
+ ) => {
try {
if (!influencerApplication.application) return;
From ff10920a8e7a4cc743ca3cf6077ebe6fd8d7277e Mon Sep 17 00:00:00 2001
From: Manan Sharma <112090668+Manan-Sharma-5@users.noreply.github.com>
Date: Tue, 7 Jan 2025 21:33:00 +0530
Subject: [PATCH 13/17] ui fix
---
styles/CollaborationDetails.styles.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/styles/CollaborationDetails.styles.tsx b/styles/CollaborationDetails.styles.tsx
index 61223820..fbcc0f10 100644
--- a/styles/CollaborationDetails.styles.tsx
+++ b/styles/CollaborationDetails.styles.tsx
@@ -30,7 +30,6 @@ export const stylesFn = (theme: Theme) =>
fontSize: 20,
color: Colors(theme).text,
lineHeight: 20,
- marginTop: 16,
},
brandName: {
fontSize: 16,
From d75c6269c646998ad30585cd4d0cdb54a370a8e5 Mon Sep 17 00:00:00 2001
From: Devender Singh Rathore <59788555+dev-rathore@users.noreply.github.com>
Date: Wed, 8 Jan 2025 14:41:55 +0530
Subject: [PATCH 14/17] Redirection fixes (#59)
* fix: initial redirections
* scheme change
* router navigate fix
* router navigate fix
---
app.json | 4 +++-
app/_layout.tsx | 34 ++++++++++++++++++------------
contexts/auth-context.provider.tsx | 2 +-
3 files changed, 25 insertions(+), 15 deletions(-)
diff --git a/app.json b/app.json
index 438010eb..eabd857e 100644
--- a/app.json
+++ b/app.json
@@ -5,7 +5,9 @@
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.jpg",
- "scheme": "myapp",
+ "scheme": [
+ "trendly-brands"
+ ],
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/images/splash.png",
diff --git a/app/_layout.tsx b/app/_layout.tsx
index 5ac5c84e..fa4d8e0f 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -36,6 +36,7 @@ import { BrandContextProvider } from "@/contexts/brand-context.provider";
import CustomPaperTheme from "@/constants/Themes/Theme";
import { queryParams } from "@/utils/url";
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
+import { Platform } from "react-native";
export {
// Catch any errors thrown by the Layout component.
@@ -116,19 +117,26 @@ const RootLayoutStack = () => {
if (isLoading) return;
- if (session && (inMainGroup || inOnboardingGroup)) {
- // Redirect to main group path if signed in
- //@ts-ignore
- router.replace(`${pathname}${queryParams(params)}`);
- } else if (session) {
- // Redirect to main group if signed in
- router.replace("/explore-influencers");
- } else if (!session && !inAuthGroup) {
- // App should start at pre-signin
- router.replace("/pre-signin");
- } else if (!session && inMainGroup) {
- // User can't access main group if not signed in
- router.replace("/login");
+ if (Platform.OS !== "web") {
+ if (session && (inMainGroup || inOnboardingGroup)) {
+ router.replace(`${pathname}${queryParams(params)}`);
+ } else if (session) {
+ router.replace("/explore-influencers");
+ } else {
+ router.replace("/pre-signin");
+ }
+ } else {
+ if (!session) {
+ router.replace("/pre-signin");
+ } else if (
+ session && (
+ pathname === "/"
+ || pathname === "/pre-signin"
+ || inAuthGroup
+ )
+ ) {
+ router.replace("/explore-influencers");
+ }
}
}, [session, isLoading]);
diff --git a/contexts/auth-context.provider.tsx b/contexts/auth-context.provider.tsx
index 135457fb..c4ead0ec 100644
--- a/contexts/auth-context.provider.tsx
+++ b/contexts/auth-context.provider.tsx
@@ -224,7 +224,7 @@ export const AuthContextProvider: React.FC = ({
email: manager?.email,
});
- router.replace("/pre-signin");
+ resetAndNavigate("/pre-signin");
Toaster.success("Signed Out Successfully!");
})
.catch((error) => {
From 71f97789c51790e55a355cca1cf205eb22b3fee2 Mon Sep 17 00:00:00 2001
From: Devender Singh Rathore <59788555+dev-rathore@users.noreply.github.com>
Date: Thu, 9 Jan 2025 10:10:22 +0530
Subject: [PATCH 15/17] Redirection changes (#61)
* redirection changes
* redirection changes
---
app/(auth)/_layout.tsx | 6 +++
app/{(public) => (auth)}/pre-signin.tsx | 0
.../(onboarding)/onboarding-get-started.tsx | 0
.../(onboarding)/onboarding-your-brand.tsx | 2 +-
app/(public)/_layout.tsx | 31 -------------
app/_layout.tsx | 43 +++++++------------
.../drawer-layout/DrawerMenuContent.tsx | 2 +-
contexts/auth-context.provider.tsx | 2 +-
8 files changed, 25 insertions(+), 61 deletions(-)
rename app/{(public) => (auth)}/pre-signin.tsx (100%)
rename app/{ => (main)/(screens)}/(onboarding)/onboarding-get-started.tsx (100%)
rename app/{ => (main)/(screens)}/(onboarding)/onboarding-your-brand.tsx (98%)
delete mode 100644 app/(public)/_layout.tsx
diff --git a/app/(auth)/_layout.tsx b/app/(auth)/_layout.tsx
index 6a6e1c8e..e224a06e 100644
--- a/app/(auth)/_layout.tsx
+++ b/app/(auth)/_layout.tsx
@@ -16,6 +16,12 @@ const AuthLayout = () => {
headerShown: false,
}}
>
+
{
role,
}).then(() => {
router.replace({
- pathname: "/(onboarding)/onboarding-get-started",
+ pathname: "/onboarding-get-started",
params: {
brandId: docRef.id,
firstBrand: firstBrand === "true" ? "true" : "false",
diff --git a/app/(public)/_layout.tsx b/app/(public)/_layout.tsx
deleted file mode 100644
index 7e5faa4c..00000000
--- a/app/(public)/_layout.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { View } from "react-native";
-import AppLayout from "@/layouts/app-layout";
-import { Stack } from "expo-router";
-
-const PublicLayout = () => {
- return (
-
-
-
-
-
-
-
- );
-};
-
-export default PublicLayout;
diff --git a/app/_layout.tsx b/app/_layout.tsx
index fa4d8e0f..ac244fcb 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -37,6 +37,7 @@ import CustomPaperTheme from "@/constants/Themes/Theme";
import { queryParams } from "@/utils/url";
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import { Platform } from "react-native";
+import { resetAndNavigate } from "@/utils/router";
export {
// Catch any errors thrown by the Layout component.
@@ -113,31 +114,23 @@ const RootLayoutStack = () => {
useEffect(() => {
const inAuthGroup = segments[0] === "(auth)";
const inMainGroup = segments[0] === "(main)";
- const inOnboardingGroup = segments[0] === "(onboarding)";
if (isLoading) return;
- if (Platform.OS !== "web") {
- if (session && (inMainGroup || inOnboardingGroup)) {
- router.replace(`${pathname}${queryParams(params)}`);
- } else if (session) {
- router.replace("/explore-influencers");
- } else {
- router.replace("/pre-signin");
- }
- } else {
- if (!session) {
- router.replace("/pre-signin");
- } else if (
- session && (
- pathname === "/"
- || pathname === "/pre-signin"
- || inAuthGroup
- )
- ) {
- router.replace("/explore-influencers");
- }
+ if (
+ session
+ && (inAuthGroup || pathname === "/")
+ ) {
+ // On boot up, session exist and user is in auth group or /, redirect to collaborations
+ resetAndNavigate("/explore-influencers");
+ } else if (
+ !session
+ && (inMainGroup || pathname === "/")
+ ) {
+ // On boot up, session doesn't exist and user is in main group or /, redirect to pre-signin
+ resetAndNavigate("/pre-signin");
}
+ // Redirect to respective screen
}, [session, isLoading]);
return (
@@ -149,12 +142,8 @@ const RootLayoutStack = () => {
headerShown: false,
}}
>
-
- {session ? (
-
- ) : (
-
- )}
+
+
= () => {
showChevron={false}
onPress={() => {
router.push({
- pathname: "/(onboarding)/onboarding-your-brand",
+ pathname: "/onboarding-your-brand",
});
navigation.dispatch(DrawerActions.closeDrawer());
}}
diff --git a/contexts/auth-context.provider.tsx b/contexts/auth-context.provider.tsx
index c4ead0ec..c719e2ba 100644
--- a/contexts/auth-context.provider.tsx
+++ b/contexts/auth-context.provider.tsx
@@ -176,7 +176,7 @@ export const AuthContextProvider: React.FC = ({
if (userCredential.user.emailVerified) {
setSession(userCredential.user.uid);
resetAndNavigate({
- pathname: "/(onboarding)/onboarding-your-brand",
+ pathname: "/onboarding-your-brand",
params: {
firstBrand: "true",
},
From f7177f17e4f98becdae8156160d2c8509fb98ac6 Mon Sep 17 00:00:00 2001
From: Manan Sharma <112090668+Manan-Sharma-5@users.noreply.github.com>
Date: Thu, 9 Jan 2025 15:46:43 +0530
Subject: [PATCH 16/17] image component and other fixes
---
.../(onboarding)/onboarding-your-brand.tsx | 62 +++++++++----------
components/InfluencerCard.tsx | 54 +++++++++-------
.../brand-profile/members-card/index.tsx | 9 ++-
.../card-components/CollaborationHeader.tsx | 8 ++-
.../OverviewTabContent.tsx | 20 +++---
.../modal/BrandModal.tsx | 8 ++-
.../modal/ManagerModal.tsx | 10 ++-
components/contract-card/ContractHeader.tsx | 9 ++-
components/contracts/ActionContainer.tsx | 17 +++--
components/contracts/AddMemberModal.tsx | 8 ++-
components/menu/index.tsx | 16 +++--
components/profile/index.tsx | 20 +++---
components/ui/screen-header/index.tsx | 56 ++++++++---------
contexts/auth-context.provider.tsx | 9 +--
14 files changed, 175 insertions(+), 131 deletions(-)
diff --git a/app/(main)/(screens)/(onboarding)/onboarding-your-brand.tsx b/app/(main)/(screens)/(onboarding)/onboarding-your-brand.tsx
index 98ef079f..83efd19a 100644
--- a/app/(main)/(screens)/(onboarding)/onboarding-your-brand.tsx
+++ b/app/(main)/(screens)/(onboarding)/onboarding-your-brand.tsx
@@ -1,7 +1,5 @@
import React, { useState } from "react";
-import {
- View,
-} from "react-native";
+import { View } from "react-native";
import { Button } from "react-native-paper";
import { router, useLocalSearchParams } from "expo-router";
import { useTheme } from "@react-navigation/native";
@@ -38,18 +36,10 @@ const OnboardingScreen = () => {
const [isSubmitting, setIsSubmitting] = useState(false);
const theme = useTheme();
const styles = fnStyles(theme);
- const {
- firstBrand,
- } = useLocalSearchParams();
- const {
- uploadImageBytes,
- } = useFirebaseStorageContext();
- const {
- setSelectedBrand,
- } = useBrandContext();
- const {
- manager: user,
- } = useAuthContext();
+ const { firstBrand } = useLocalSearchParams();
+ const { uploadImageBytes } = useFirebaseStorageContext();
+ const { setSelectedBrand } = useBrandContext();
+ const { manager: user } = useAuthContext();
const handleCreateBrand = async () => {
if (!user) {
@@ -58,14 +48,17 @@ const OnboardingScreen = () => {
}
if (!brandData.name) {
- Toaster.error('Brand name is required');
+ Toaster.error("Brand name is required");
return;
}
let imageUrl = "";
if (brandData.image) {
const blob = await fetch(brandData.image).then((res) => res.blob());
- imageUrl = await uploadImageBytes(blob, `brands/${brandData.name}-${Date.now()}`);
+ imageUrl = await uploadImageBytes(
+ blob,
+ `brands/${brandData.name}-${Date.now()}`
+ );
}
if (user) {
@@ -88,23 +81,26 @@ const OnboardingScreen = () => {
await setDoc(managerRef, {
managerId: user.id,
- role,
- }).then(() => {
- router.replace({
- pathname: "/onboarding-get-started",
- params: {
- brandId: docRef.id,
- firstBrand: firstBrand === "true" ? "true" : "false",
- },
+ role: "Manager",
+ })
+ .then(() => {
+ router.replace({
+ pathname: "/onboarding-get-started",
+ params: {
+ brandId: docRef.id,
+ firstBrand: firstBrand === "true" ? "true" : "false",
+ },
+ });
+ setSelectedBrand(brandData as Brand);
+ })
+ .catch((error) => {
+ Toaster.error("Error creating brand");
+ })
+ .finally(() => {
+ setIsSubmitting(false);
});
- setSelectedBrand(brandData as Brand);
- }).catch((error) => {
- Toaster.error("Error creating brand");
- }).finally(() => {
- setIsSubmitting(false);
- });
}
- }
+ };
return (
@@ -129,7 +125,7 @@ const OnboardingScreen = () => {
type="create"
/>
-
+
);
};
diff --git a/components/InfluencerCard.tsx b/components/InfluencerCard.tsx
index 26fdb704..bd7ec044 100644
--- a/components/InfluencerCard.tsx
+++ b/components/InfluencerCard.tsx
@@ -13,7 +13,15 @@ import { useTheme } from "@react-navigation/native";
import Colors from "@/constants/Colors";
import { convertToKUnits } from "@/utils/conversion";
import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome";
-import { faEllipsis, faPeopleRoof, faChartLine, faFaceSmile, faComment, faCheck, faPlus } from "@fortawesome/free-solid-svg-icons";
+import {
+ faEllipsis,
+ faPeopleRoof,
+ faChartLine,
+ faFaceSmile,
+ faComment,
+ faCheck,
+ faPlus,
+} from "@fortawesome/free-solid-svg-icons";
import { imageUrl } from "@/utils/url";
import Tag from "./ui/tag";
import { User } from "@/types/User";
@@ -23,6 +31,7 @@ import { truncateText } from "@/utils/text";
import { MediaItem } from "@/shared-uis/components/carousel/render-media-item";
import Carousel from "@/shared-uis/components/carousel/carousel";
import AssetPreviewModal from "@/shared-uis/components/carousel/asset-preview-modal";
+import ImageComponent from "@/shared-uis/components/image-component";
interface InfluencerCardPropsType {
alreadyInvited?: (influencerId: string) => Promise;
@@ -48,7 +57,7 @@ const InfluencerCard = (props: InfluencerCardPropsType) => {
const onImagePress = (data: MediaItem) => {
setPreviewImageUrl(data.url);
setPreviewImage(true);
- }
+ };
useEffect(() => {
if (props?.alreadyInvited) {
@@ -60,15 +69,8 @@ const InfluencerCard = (props: InfluencerCardPropsType) => {
return (
<>
-
-
+
+
{
if (props.openProfile) {
@@ -76,10 +78,12 @@ const InfluencerCard = (props: InfluencerCardPropsType) => {
}
}}
>
-
{
}}
>
{influencer.name}
- {influencer.socials?.[0] || 'influencer-handle'}
+
+ {influencer.socials?.[0] || "influencer-handle"}
+
{props.type === "invitation" &&
(isInvited ? (
@@ -141,14 +147,16 @@ const InfluencerCard = (props: InfluencerCardPropsType) => {
processRawAttachment(attachment)) || []}
+ data={
+ influencer.profile?.attachments?.map((attachment) =>
+ processRawAttachment(attachment)
+ ) || []
+ }
onImagePress={onImagePress}
theme={theme}
/>
-
+
@@ -203,8 +211,10 @@ const InfluencerCard = (props: InfluencerCardPropsType) => {
contentWidth={screenWidth}
source={{
html:
- truncateText(influencer?.profile?.content?.about as string, 160) ||
- "No content available.
",
+ truncateText(
+ influencer?.profile?.content?.about as string,
+ 160
+ ) || "No content available.
",
}}
defaultTextProps={{
style: {
diff --git a/components/brand-profile/members-card/index.tsx b/components/brand-profile/members-card/index.tsx
index 90b6f6b3..f188417c 100644
--- a/components/brand-profile/members-card/index.tsx
+++ b/components/brand-profile/members-card/index.tsx
@@ -2,6 +2,7 @@ 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 ImageComponent from "@/shared-uis/components/image-component";
import { imageUrl } from "@/utils/url";
import { faEllipsisH } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome";
@@ -46,7 +47,13 @@ const MembersCard: FC = ({ manager, cardType, action }) => {
gap: 10,
}}
>
-
+
{manager.name}
{manager.email}
diff --git a/components/collaboration-card/card-components/CollaborationHeader.tsx b/components/collaboration-card/card-components/CollaborationHeader.tsx
index c0e35118..99abf85f 100644
--- a/components/collaboration-card/card-components/CollaborationHeader.tsx
+++ b/components/collaboration-card/card-components/CollaborationHeader.tsx
@@ -7,6 +7,7 @@ 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 ImageComponent from "@/shared-uis/components/image-component";
interface CollaborationHeaderProps {
collabName: string;
@@ -47,8 +48,11 @@ const CollaborationHeader: FC = ({
gap: 8,
}}
>
- {
flexGrow: 1,
}}
>
-
@@ -442,12 +446,12 @@ const OverviewTabContent = (props: CollaborationDetailsContentProps) => {
gap: 10,
}}
>
- = ({
>
{/* Brand Image */}
- = ({
>
{/* Brand Image */}
-
{/* Brand Name */}
diff --git a/components/contract-card/ContractHeader.tsx b/components/contract-card/ContractHeader.tsx
index af027d3d..fd2c2773 100644
--- a/components/contract-card/ContractHeader.tsx
+++ b/components/contract-card/ContractHeader.tsx
@@ -8,6 +8,7 @@ import Colors from "@/constants/Colors";
import { faEllipsisH } from "@fortawesome/free-solid-svg-icons";
import { useTheme } from "@react-navigation/native";
import { imageUrl } from "@/utils/url";
+import ImageComponent from "@/shared-uis/components/image-component";
interface ContractHeaderProps {
username: string;
@@ -46,8 +47,12 @@ const ContractHeader: FC = ({
gap: 8,
}}
>
- = ({
flexGrow: 1,
}}
>
-
@@ -263,8 +268,12 @@ const ActionContainer: FC = ({
flexGrow: 1,
}}
>
-
diff --git a/components/contracts/AddMemberModal.tsx b/components/contracts/AddMemberModal.tsx
index c27199fc..7eb9b718 100644
--- a/components/contracts/AddMemberModal.tsx
+++ b/components/contracts/AddMemberModal.tsx
@@ -20,6 +20,7 @@ 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";
+import ImageComponent from "@/shared-uis/components/image-component";
interface AddMemberModalProps {
visible: boolean;
@@ -111,7 +112,12 @@ const AddMembersModal: FC = ({
const renderItem = (item: any) => (
-
+
{
const theme = useTheme();
@@ -90,15 +91,12 @@ const Menu = () => {
}}
>
-
diff --git a/components/profile/index.tsx b/components/profile/index.tsx
index d6985342..6c060c87 100644
--- a/components/profile/index.tsx
+++ b/components/profile/index.tsx
@@ -29,6 +29,7 @@ import {
} from "firebase/storage";
import { StorageApp } from "@/utils/firebase-storage";
import { useBrandContext } from "@/contexts/brand-context.provider";
+import ImageComponent from "@/shared-uis/components/image-component";
const Profile = () => {
const [name, setName] = useState("");
@@ -55,7 +56,7 @@ const Profile = () => {
setLoading(true);
try {
- let updatedImageURL = manager.profileImage; // Default to existing profile image
+ let updatedImageURL = manager.profileImage || ""; // Default to existing profile image
if (imageToUpload) {
const path = `managers/${manager.id}/profile-image`;
@@ -90,6 +91,7 @@ const Profile = () => {
setImageToUpload(""); // Clear image-to-upload buffer
Toaster.success("Profile updated successfully");
+ fetchRole();
} catch (error) {
console.error("Error during profile update:", error);
Toaster.error("Error during profile update");
@@ -158,15 +160,13 @@ const Profile = () => {
>
{/* Profile Picture */}
- = ({
} else {
navigation.goBack();
}
- }
+ };
return (
= ({
}}
statusBarHeight={0}
>
- {
- !hideAction && (
- (
-
-
-
- )}
- color={Colors(theme).text}
- onPress={handleAction}
- />
- )
- }
+ {!hideAction && (
+
+
+
+
+
+ )}
-
+
- {
- rightAction && rightActionButton
- }
+ {rightAction && rightActionButton}
);
};
diff --git a/contexts/auth-context.provider.tsx b/contexts/auth-context.provider.tsx
index c719e2ba..bdef968a 100644
--- a/contexts/auth-context.provider.tsx
+++ b/contexts/auth-context.provider.tsx
@@ -162,7 +162,6 @@ export const AuthContextProvider: React.FC = ({
emailNotification: true,
pushNotification: true,
},
- role: "manager",
};
await setDoc(docRef, userData);
@@ -181,8 +180,8 @@ export const AuthContextProvider: React.FC = ({
firstBrand: "true",
},
});
- 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}`,
@@ -248,7 +247,9 @@ export const AuthContextProvider: React.FC = ({
return null;
};
- const getInfluencerById = async (influencerId: string): Promise => {
+ const getInfluencerById = async (
+ influencerId: string
+ ): Promise => {
const userRef = doc(FirestoreDB, "users", influencerId);
const userSnap = await getDoc(userRef);
From 3a7ff25cba50fb637eb7f848782f91f325079e25 Mon Sep 17 00:00:00 2001
From: Devender Singh Rathore <59788555+dev-rathore@users.noreply.github.com>
Date: Fri, 10 Jan 2025 13:49:09 +0530
Subject: [PATCH 17/17] No brands protected screen implementation (#64)
* added no brands protected screen and shimmer ui
* shimmer ui update
* added loading indicator
---
app/(main)/(drawer)/_layout.tsx | 31 ++---
.../(onboarding)/onboarding-your-brand.tsx | 3 +
app/index.tsx | 19 ++-
.../shimmers/explore-influencer-shimmer.tsx | 110 ++++++++++++++++++
constants/Colors.ts | 4 +
layouts/protected/brand-protected-screen.tsx | 92 +++++++++++++++
layouts/protected/index.ts | 3 +
shared-uis | 2 +-
8 files changed, 247 insertions(+), 17 deletions(-)
create mode 100644 components/shimmers/explore-influencer-shimmer.tsx
create mode 100644 layouts/protected/brand-protected-screen.tsx
create mode 100644 layouts/protected/index.ts
diff --git a/app/(main)/(drawer)/_layout.tsx b/app/(main)/(drawer)/_layout.tsx
index cf44cba1..f752b0a3 100644
--- a/app/(main)/(drawer)/_layout.tsx
+++ b/app/(main)/(drawer)/_layout.tsx
@@ -2,27 +2,30 @@ import { Drawer } from "expo-router/drawer";
import DrawerMenuContent from "@/components/drawer-layout/DrawerMenuContent";
import BackButton from "@/components/ui/back-button/BackButton";
import { useBreakpoints } from "@/hooks";
+import { BrandProtectedScreen } from "@/layouts/protected";
const DrawerLayout = () => {
const { xl } = useBreakpoints();
return (
- }
- screenOptions={{
- drawerType: xl ? "permanent" : "slide",
- headerShown: false,
- headerLeft: () => xl ? null : ,
- }}
- >
-
+ }
+ screenOptions={{
+ drawerType: xl ? "permanent" : "slide",
headerShown: false,
+ headerLeft: () => xl ? null : ,
}}
- />
-
+ >
+
+
+
);
};
diff --git a/app/(main)/(screens)/(onboarding)/onboarding-your-brand.tsx b/app/(main)/(screens)/(onboarding)/onboarding-your-brand.tsx
index 98ef079f..f34f2df9 100644
--- a/app/(main)/(screens)/(onboarding)/onboarding-your-brand.tsx
+++ b/app/(main)/(screens)/(onboarding)/onboarding-your-brand.tsx
@@ -52,13 +52,16 @@ const OnboardingScreen = () => {
} = useAuthContext();
const handleCreateBrand = async () => {
+ setIsSubmitting(true);
if (!user) {
Toaster.error("User not found");
+ setIsSubmitting(false);
return;
}
if (!brandData.name) {
Toaster.error('Brand name is required');
+ setIsSubmitting(false);
return;
}
diff --git a/app/index.tsx b/app/index.tsx
index c6d5f68b..caf37c26 100644
--- a/app/index.tsx
+++ b/app/index.tsx
@@ -1,8 +1,23 @@
-import { View } from "@/components/theme/Themed"
+import { Image } from "react-native";
+import { View } from "@/components/theme/Themed";
const Index = () => {
return (
-
+
+
+
);
};
diff --git a/components/shimmers/explore-influencer-shimmer.tsx b/components/shimmers/explore-influencer-shimmer.tsx
new file mode 100644
index 00000000..3ce5649f
--- /dev/null
+++ b/components/shimmers/explore-influencer-shimmer.tsx
@@ -0,0 +1,110 @@
+import React from 'react';
+import { StyleSheet } from 'react-native';
+import { useTheme } from '@react-navigation/native';
+import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome';
+import { faBell, faChevronDown } from '@fortawesome/free-solid-svg-icons';
+
+import AppLayout from '@/layouts/app-layout';
+import Colors from '@/constants/Colors';
+import Shimmer from '@/shared-uis/components/shimmer';
+import { View } from '../theme/Themed';
+
+const ExploreInfluencerShimmer = () => {
+ const theme = useTheme();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {[...Array(5)].map((_, index) => (
+
+ ))}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ paddingHorizontal: 16,
+ },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 16,
+ },
+ headerLeft: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 12,
+ },
+ searchContainer: {
+ marginBottom: 16,
+ },
+ profileSection: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 12,
+ marginBottom: 16,
+ },
+ profileInfo: {
+ gap: 4,
+ },
+ metrics: {
+ flexDirection: 'row',
+ gap: 16,
+ marginVertical: 16,
+ },
+ bottomNav: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ gap: 16,
+ marginTop: 'auto',
+ },
+});
+
+export default ExploreInfluencerShimmer;
diff --git a/constants/Colors.ts b/constants/Colors.ts
index 3c6b19ce..120026c9 100644
--- a/constants/Colors.ts
+++ b/constants/Colors.ts
@@ -19,6 +19,8 @@ export default (theme: Theme) => ({
tag: "#5f6368",
tagForeground: "#f1f3f4",
outline: "#538BA6",
+ shimmerBackground: "#1E1E1E",
+ shimmerOverlay: "#1b1b1b",
}
: {
card: "#fff",
@@ -33,6 +35,8 @@ export default (theme: Theme) => ({
tag: "#f1f3f4",
tagForeground: "#5f6368",
outline: "#054463",
+ shimmerBackground: "#f1f3f4",
+ shimmerOverlay: "#eee",
}),
aliceBlue: "#E9F1F7",
amber: "#FFBF00",
diff --git a/layouts/protected/brand-protected-screen.tsx b/layouts/protected/brand-protected-screen.tsx
new file mode 100644
index 00000000..2270705e
--- /dev/null
+++ b/layouts/protected/brand-protected-screen.tsx
@@ -0,0 +1,92 @@
+import React, { PropsWithChildren, useEffect, useState } from "react";
+import { ActivityIndicator, Platform } from "react-native";
+import { useTheme } from "@react-navigation/native";
+import { useRouter } from "expo-router";
+import { collection, getDocs, query, where } from "firebase/firestore";
+
+import { View } from "@/components/theme/Themed";
+import { useAuthContext } from "@/contexts";
+import Colors from "@/constants/Colors";
+import ExploreInfluencerShimmer from "@/components/shimmers/explore-influencer-shimmer";
+import { FirestoreDB } from "@/utils/firestore";
+
+interface BrandProtectedScreenProps extends PropsWithChildren { }
+
+const BrandProtectedScreen: React.FC = ({
+ children,
+}) => {
+ const [hasBrandWithManagerId, setHasBrandWithManagerId] = useState(false);
+ const router = useRouter();
+
+ const {
+ manager,
+ } = useAuthContext();
+
+ const fetchBrands = async (
+ managerId: string
+ ): Promise => {
+ const brandsCollection = collection(FirestoreDB, "brands");
+
+ const brandsSnapshot = await getDocs(brandsCollection);
+
+ for (const brandDoc of brandsSnapshot.docs) {
+ const brandId = brandDoc.id;
+
+ const membersCollection = collection(FirestoreDB, `brands/${brandId}/members`);
+ const membersQuery = query(membersCollection, where("managerId", "==", managerId));
+ const membersSnapshot = await getDocs(membersQuery);
+
+ if (!membersSnapshot.empty) {
+ setHasBrandWithManagerId(true);
+ return;
+ }
+ }
+
+ // If the manager has no brands, redirect to onboarding
+ router.replace({
+ pathname: "/onboarding-your-brand",
+ params: {
+ firstBrand: "true",
+ },
+ });
+ };
+
+ useEffect(() => {
+ if (!manager?.id) return;
+
+ fetchBrands(manager.id);
+ }, [manager]);
+
+ if (!hasBrandWithManagerId) {
+ return ;
+ }
+
+ return <>{children}>;
+};
+
+const LoadingScreen: React.FC = () => {
+ const theme = useTheme();
+
+ if (Platform.OS === "web") {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+export default BrandProtectedScreen;
diff --git a/layouts/protected/index.ts b/layouts/protected/index.ts
new file mode 100644
index 00000000..b611b9f2
--- /dev/null
+++ b/layouts/protected/index.ts
@@ -0,0 +1,3 @@
+import BrandProtectedScreen from "./brand-protected-screen";
+
+export { BrandProtectedScreen };
diff --git a/shared-uis b/shared-uis
index f7719b1e..e0719223 160000
--- a/shared-uis
+++ b/shared-uis
@@ -1 +1 @@
-Subproject commit f7719b1e63141841d9eabfc029a7eefedc5ae669
+Subproject commit e071922330d1f2682e4088d152f4416395160e1f