Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions app/(modal)/create-collaboration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const CreateCollaborationScreen = () => {
},
]);
const [location, setLocation] = useState("Remote");
const [formattedAddress, setFormattedAddress] = useState("");
const [links, setLinks] = useState<any[]>([]);
const [screen, setScreen] = useState(1);
const theme = useTheme();
Expand Down Expand Up @@ -89,6 +90,10 @@ const CreateCollaborationScreen = () => {
setIsModalVisible(false);
};

const onFormattedAddressChange = (address: string) => {
setFormattedAddress(address);
}

const submitCollaboration = async () => {
try {
if (!AuthApp.currentUser) {
Expand All @@ -111,6 +116,18 @@ const CreateCollaborationScreen = () => {
}

const collabRef = collection(FirestoreDB, "collaborations");

let locationAddress = {};
if (location === "Physical" && mapRegion.latitude && mapRegion.longitude) {
locationAddress = {
name: formattedAddress,
latlong: {
lat: mapRegion.latitude,
long: mapRegion.longitude,
},
};
}
Comment on lines +119 to +129

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add coordinate range validation

While the location handling is structured well, consider adding validation for coordinate ranges to ensure they are within valid bounds (-90 to 90 for latitude, -180 to 180 for longitude).

 let locationAddress = {};
 if (location === "Physical" && mapRegion.latitude && mapRegion.longitude) {
+  const isValidLatitude = mapRegion.latitude >= -90 && mapRegion.latitude <= 90;
+  const isValidLongitude = mapRegion.longitude >= -180 && mapRegion.longitude <= 180;
+  if (!isValidLatitude || !isValidLongitude) {
+    Toaster.error("Invalid coordinates");
+    return;
+  }
   locationAddress = {
     name: formattedAddress,
     latlong: {
       lat: mapRegion.latitude,
       long: mapRegion.longitude,
     },
   };
 }

Committable suggestion skipped: line range outside the PR's diff.


await addDoc(collabRef, {
name: collaborationName,
brandId: selectedBrand ? selectedBrand.id : "",
Expand All @@ -127,13 +144,7 @@ const CreateCollaborationScreen = () => {
platform: platform[0].value,
location: {
type: location,
...(location === "Physical" && {
name: "locationName", // TODO: Add location name using geocoding API
latlong: {
lat: mapRegion.latitude,
long: mapRegion.longitude,
},
}),
...locationAddress,
},
externalLinks: links,
status: "active",
Expand Down Expand Up @@ -371,6 +382,7 @@ const CreateCollaborationScreen = () => {
<CreateCollaborationMap
mapRegion={mapRegion}
onMapRegionChange={(region) => setMapRegion(region)}
onFormattedAddressChange={onFormattedAddressChange}
/>
)
}
Expand Down
19 changes: 18 additions & 1 deletion app/(onboarding)/onboarding-your-brand.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { addDoc, collection, doc, setDoc } from "firebase/firestore";
import { FirestoreDB } from "@/utils/firestore";
import Toaster from "@/shared-uis/components/toaster/Toaster";
import Colors from "@/constants/Colors";
import { useFirebaseStorageContext } from "@/contexts";

const OnboardingScreen = () => {
const [brandName, setBrandName] = useState("");
Expand All @@ -29,11 +30,15 @@ const OnboardingScreen = () => {
const [filteredRoles, setFilteredRoles] = useState<any[]>([]);
const [filteredIndustries, setFilteredIndustries] = useState<string[]>([]);
const [activeDropdown, setActiveDropdown] = useState<any>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const theme = useTheme();
const styles = fnStyles(theme);
const {
firstBrand,
} = useLocalSearchParams();
const {
uploadImageBytes,
} = useFirebaseStorageContext();

const pickImage = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
Expand All @@ -42,6 +47,7 @@ const OnboardingScreen = () => {
aspect: [4, 3],
quality: 1,
});

if (!result.canceled) {
setImage(result.assets[0].uri);
}
Comment on lines +50 to 53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance image validation and error handling

The image picker implementation should include size and format validation to prevent issues during upload.

  if (!result.canceled) {
+   const { uri, fileSize, type } = result.assets[0];
+   const MAX_SIZE = 5 * 1024 * 1024; // 5MB
+   if (fileSize > MAX_SIZE) {
+     Toaster.error("Image size should be less than 5MB");
+     return;
+   }
+   if (!type?.startsWith('image/')) {
+     Toaster.error("Invalid file format");
+     return;
+   }
-   setImage(result.assets[0].uri);
+   setImage(uri);
  }

Committable suggestion skipped: line range outside the PR's diff.

Expand All @@ -67,17 +73,26 @@ const OnboardingScreen = () => {

const handleSubmit = async () => {
try {
setIsSubmitting(true);
const user = AuthApp.currentUser;
if (!brandName || !role || !industry || !website) {
Toaster.error("Please fill in all fields.");
setIsSubmitting(false);
return;
}

let imageUrl = "";
if (image) {
const blob = await fetch(image).then((res) => res.blob());
imageUrl = await uploadImageBytes(blob, `brands/${brandName}-${Date.now()}`);
}

Comment on lines +76 to +89

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Improve form submission robustness and error handling

The current implementation has several potential issues that should be addressed:

  1. No cleanup of uploaded images if form submission fails
  2. No type checking for blob conversion
  3. Potential memory leak if component unmounts during upload
+ const [uploadError, setUploadError] = useState<Error | null>(null);
+ useEffect(() => {
+   return () => {
+     // Cleanup effect for component unmount
+     if (isSubmitting) {
+       // Cancel ongoing uploads if necessary
+     }
+   };
+ }, [isSubmitting]);

  const handleSubmit = async () => {
    try {
      setIsSubmitting(true);
+     setUploadError(null);
      const user = AuthApp.currentUser;
      if (!brandName || !role || !industry || !website) {
        Toaster.error("Please fill in all fields.");
        setIsSubmitting(false);
        return;
      }

      let imageUrl = "";
      if (image) {
+       try {
          const blob = await fetch(image).then((res) => res.blob());
+         if (!(blob instanceof Blob)) {
+           throw new Error('Invalid image format');
+         }
          imageUrl = await uploadImageBytes(blob, `brands/${brandName}-${Date.now()}`);
+       } catch (error) {
+         // If image upload fails, cleanup and show error
+         setUploadError(error as Error);
+         setIsSubmitting(false);
+         Toaster.error("Failed to upload image");
+         return;
+       }
      }

Committable suggestion skipped: line range outside the PR's diff.

if (user) {
const colRef = collection(FirestoreDB, "brands");

let brandData = {
name: brandName,
image: "",
image: imageUrl,
profile: {
industry,
website,
Expand All @@ -99,6 +114,7 @@ const OnboardingScreen = () => {
role,
});

setIsSubmitting(false);
router.navigate({
pathname: "/(onboarding)/onboarding-brand-preference",
params: { brandId: docRef.id },
Expand Down Expand Up @@ -238,6 +254,7 @@ const OnboardingScreen = () => {
/>

<Button
loading={isSubmitting}
mode="contained"
onPress={handleSubmit}
>
Expand Down
10 changes: 4 additions & 6 deletions components/InfluencerCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome";
import { faBullseye, faStar, faUsers, faMessage, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { imageUrl } from "@/utils/url";
import Tag from "./ui/tag";
import { MediaItem } from "./ui/carousel/render-media-item";

const { width } = Dimensions.get("window");

Expand All @@ -38,10 +39,7 @@ interface InfluencerCardPropsType {
name: string;
handle: string;
profilePic: string;
media: {
type: string;
uri: string;
}[];
media: MediaItem[];
followers: number | string;
reach: number | string;
rating: number | string;
Expand Down Expand Up @@ -98,8 +96,8 @@ const InfluencerCard = (props: InfluencerCardPropsType) => {
};
});

const onImagePress = (data: any) => {
setSelectedImage(data); // TODO: data -> uri
const onImagePress = (data: MediaItem) => {
setSelectedImage(data.url);
setIsZoomed(true);
}

Expand Down
9 changes: 0 additions & 9 deletions components/brand/CreateBrand.tsx

This file was deleted.

31 changes: 20 additions & 11 deletions components/collaboration/CollaborationHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import Tag from '../ui/tag';
import { Text, View } from '../theme/Themed';
import { useTheme } from '@react-navigation/native';
import { stylesFn } from '@/styles/collaboration-details/CollaborationHeader.styles';
import { CollaborationDetail } from './collaboration-details';

interface ColloborationHeaderProps {
collaboration: any;
collaboration: CollaborationDetail;
}

const CollaborationHeader: React.FC<ColloborationHeaderProps> = ({
Expand Down Expand Up @@ -38,16 +39,24 @@ const CollaborationHeader: React.FC<ColloborationHeaderProps> = ({
</View>

<View style={styles.tagsContainer}>
<Tag
icon="checkbox-marked-circle"
>
{collaboration.appliedCount} Applied
</Tag>
<Tag
icon="eye"
>
{collaboration.brandViewed} Reviewed
</Tag>
{
collaboration.promotionType && (
<Tag
icon="checkbox-marked-circle"
>
{collaboration.promotionType}
</Tag>
)
}
{
collaboration.collaborationType && (
<Tag
icon="eye"
>
{collaboration.collaborationType}
</Tag>
)
}
<Tag
icon="map-marker"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { collection, doc, getDoc, getDocs } from "firebase/firestore";
import BottomSheetActions from "@/components/BottomSheetActions";
import EmptyState from "@/components/ui/empty-state";
import Colors from "@/constants/Colors";
import { processRawAttachment } from "@/utils/attachments";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Type safety improvements needed for attachment processing

The processRawAttachment function in utils/attachments.ts uses any type for its parameter while returning a MediaItem. Update the function signature to use the proper MediaItem type that's already defined in the codebase:

  • utils/attachments.ts: Replace attachment: any with proper typing that matches the expected shape of the attachment parameter
  • components/collaboration/collaboration-details/ApplicationsTabContent.tsx: Remove the : any type annotation in the map((media: any) => call
🔗 Analysis chain

Verify types for attachment processing

The new import for processRawAttachment is good, but we should ensure proper type safety.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check the type definitions in the attachments utility
ast-grep --pattern 'export function processRawAttachment($_) {
  $$$
}'

# Look for any existing type definitions for attachments
rg -t typescript "type.*Attachment" 

Length of output: 146


Script:

#!/bin/bash
# Look for processRawAttachment function definition
rg -l "processRawAttachment"

# Once we find the file, let's examine its contents
rg -A 10 "processRawAttachment" 

# Look for type definitions with a broader search
rg "type.*ttachment|interface.*ttachment"

Length of output: 3491


Script:

#!/bin/bash
# Let's check for MediaItem type definition since processRawAttachment returns it
rg -A 5 "type MediaItem|interface MediaItem"

# Also check the file where MediaItem might be imported from
rg -l "MediaItem"

Length of output: 602


const ApplicationsTabContent = (props: any) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add proper TypeScript interfaces for improved type safety

The component uses any type extensively, which defeats the purpose of TypeScript. Consider adding proper interfaces:

interface ApplicationsTabContentProps {
  pageID: string;
  collaboration: CollaborationDetail;
}

interface Influencer {
  id: string;
  profileImage: string;
  name: string;
  handle: string;
  attachments: Attachment[];
  backend?: {
    followers: number;
    reach: number;
    rating: number;
  };
}

interface SelectedApplication {
  applicationID: string;
  collaborationID: string;
  influencerID: string;
}

Then update the component:

-const ApplicationsTabContent = (props: any) => {
+const ApplicationsTabContent: React.FC<ApplicationsTabContentProps> = (props) => {
-  const [influencers, setInfluencers] = useState<any>([]);
+  const [influencers, setInfluencers] = useState<Influencer[]>([]);
-  const [selectedApplication, setSelectedApplication] = useState<any>(null);
+  const [selectedApplication, setSelectedApplication] = useState<SelectedApplication | null>(null);

Also applies to: 19-22

const theme = useTheme();
Expand Down Expand Up @@ -96,7 +97,7 @@ const ApplicationsTabContent = (props: any) => {
profilePic: item.profileImage,
name: item.name,
handle: item.handle || "@handle",
media: item.attachments,
media: item.attachments.map((media: any) => processRawAttachment(media)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add type safety and error handling for attachment processing

While processing attachments is good, there are several improvements needed:

  1. Replace any with proper types
  2. Add error handling for invalid attachments
  3. Consider memoizing the processed attachments to avoid unnecessary re-processing
-media: item.attachments.map((media: any) => processRawAttachment(media)),
+media: item.attachments.map((media: Attachment) => {
+  try {
+    return processRawAttachment(media);
+  } catch (error) {
+    console.error('Failed to process attachment:', error);
+    return null;
+  }
+}).filter(Boolean),

Committable suggestion skipped: line range outside the PR's diff.

followers: item.backend ? item.backend.followers : 1000,
reach: item.backend ? item.backend.reach : 10000,
rating: item.backend ? item.backend.rating : 4.5,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ const InvitationsTabContent = (props: any) => {
media: [
{
type: "image",
uri: item.profileImage,
url: item.profileImage,
},
],
followers: item.backend ? item.backend.followers : 1000,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ import {
} from "react-native-paper";
import { useTheme } from "@react-navigation/native";
import { stylesFn } from "@/styles/collaboration-details/CollaborationDetails.styles";
import { CollaborationDetail } from ".";

interface OverviewTabContentProps {
collaboration: any;
logo: string;
collaboration: CollaborationDetail;
};

const OverviewTabContent: React.FC<OverviewTabContentProps> = ({
collaboration,
logo,
}) => {
const theme = useTheme();
const styles = stylesFn(theme);
Expand All @@ -39,7 +38,7 @@ const OverviewTabContent: React.FC<OverviewTabContentProps> = ({
<Image
source={{
uri:
logo ||
collaboration?.logo ||
"https://cdn.pixabay.com/photo/2022/09/21/17/02/blue-background-7470781_640.jpg",
}}
style={styles.profileImage}
Expand Down
12 changes: 6 additions & 6 deletions components/collaboration/collaboration-details/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import OverviewTabContent from "./OverviewTabContent";
import SettingsTabContent from "./SettingsTabContent";
import CollaborationHeader from "../CollaborationHeader";

interface Collaboration extends ICollaboration {
export interface CollaborationDetail extends ICollaboration {
brandDescription: string;
brandName: string;
logo: string;
paymentVerified: boolean;
brandDescription: string;
}
Comment on lines +14 to 19

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Interface inconsistency detected - Collaboration interface still exists

The verification revealed that the old Collaboration interface still exists in types/Collaboration.ts while the new CollaborationDetail interface is being used in multiple components. This could lead to type inconsistencies.

  • types/Collaboration.ts: Contains the original Collaboration interface
  • Multiple components are now using the new CollaborationDetail interface:
    • components/collaboration/CollaborationHeader.tsx
    • components/collaboration/collaboration-details/OverviewTabContent.tsx
    • components/collaboration/collaboration-details/index.tsx
🔗 Analysis chain

Interface changes look good, verify usage across codebase

The renaming of Collaboration to CollaborationDetail and addition of new properties is well-structured. The interface is properly exported and typed.

Let's verify the interface usage across the codebase:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for any remaining references to the old interface name and verify new interface adoption

# Search for old interface references
echo "Checking for old Collaboration interface references..."
rg "interface Collaboration extends ICollaboration" 

# Search for new interface usage
echo "Checking CollaborationDetail usage..."
rg "CollaborationDetail"

Length of output: 2217


interface CollaborationDetailsProps {
Expand All @@ -24,7 +25,7 @@ interface CollaborationDetailsProps {
const CollaborationDetails: React.FC<CollaborationDetailsProps> = ({
pageID
}) => {
const [collaboration, setCollaboration] = useState<Collaboration | undefined>(
const [collaboration, setCollaboration] = useState<CollaborationDetail | undefined>(
undefined
);
const [loading, setLoading] = useState(true);
Expand All @@ -43,6 +44,7 @@ const CollaborationDetails: React.FC<CollaborationDetailsProps> = ({

setCollaboration({
...data,
logo: brandData?.image || "",
brandName: brandData?.name || "Unknown Brand",
paymentVerified: brandData?.paymentMethodVerified || false,
brandDescription: brandData?.description || "",
Expand All @@ -60,9 +62,7 @@ const CollaborationDetails: React.FC<CollaborationDetailsProps> = ({
title: "Overview",
component: (
<OverviewTabContent
collaboration={collaboration as Collaboration}
// logo={collaboration?.logo}
logo={""}
collaboration={collaboration as CollaborationDetail}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve type safety in prop passing

The current type assertion using as could be unsafe. Consider adding runtime checks or using optional chaining.

Here's a suggested improvement:

-          collaboration={collaboration as CollaborationDetail}
+          collaboration={collaboration ?? undefined}

Committable suggestion skipped: line range outside the PR's diff.

/>
),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import { useTheme } from "@react-navigation/native";
interface CreateCollaborationMapProps {
mapRegion: any;
onMapRegionChange: (region: any) => void;
onFormattedAddressChange?: (address: string) => void;
}

const CreateCollaborationMap: React.FC<CreateCollaborationMapProps> = ({
mapRegion,
onMapRegionChange,
onFormattedAddressChange,
}) => {
const [nativeMapRegion, setNativeMapRegion] = useState(mapRegion);

Expand All @@ -24,20 +26,57 @@ const CreateCollaborationMap: React.FC<CreateCollaborationMapProps> = ({
const theme = useTheme();
const styles = stylesFn(theme);

const onLocationChange = (
const fetchMapRegionAddress = async (
lat: number,
lng: number
) => {
if (!lat || !lng) {
return;
}

const response = await fetch(
`https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&key=${process.env.EXPO_PUBLIC_GOOGLE_MAPS_API_KEY}`
);

const result = await response.json();

const address = result?.results?.[0]?.formatted_address || result?.plus_code?.compound_code || 'Address';

return address;
};
Comment on lines +29 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider improving error handling and security.

Several improvements could enhance the robustness of this function:

  1. Add proper error handling for API failures
  2. Consider moving the geocoding logic to a backend service to protect the API key
  3. Implement rate limiting or caching to prevent excessive API calls
  4. Add proper TypeScript types for the API response

Here's a suggested improvement:

  const fetchMapRegionAddress = async (
-   lat: number,
-   lng: number
+   lat: number | undefined,
+   lng: number | undefined
  ): Promise<string> => {
    if (!lat || !lng) {
-     return;
+     return 'Address';
    }

+   try {
      const response = await fetch(
        `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&key=${process.env.EXPO_PUBLIC_GOOGLE_MAPS_API_KEY}`
      );

+     if (!response.ok) {
+       throw new Error('Geocoding API request failed');
+     }

      const result = await response.json();

      const address = result?.results?.[0]?.formatted_address || result?.plus_code?.compound_code || 'Address';

      return address;
+   } catch (error) {
+     console.error('Error fetching address:', error);
+     return 'Address';
+   }
  };

Consider implementing a backend proxy for geocoding:

  1. Create an endpoint (e.g., /api/geocode) that internally calls Google Maps API
  2. Update this function to call your backend endpoint instead
  3. This protects your API key and allows for implementing rate limiting and caching
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fetchMapRegionAddress = async (
lat: number,
lng: number
) => {
if (!lat || !lng) {
return;
}
const response = await fetch(
`https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&key=${process.env.EXPO_PUBLIC_GOOGLE_MAPS_API_KEY}`
);
const result = await response.json();
const address = result?.results?.[0]?.formatted_address || result?.plus_code?.compound_code || 'Address';
return address;
};
const fetchMapRegionAddress = async (
lat: number | undefined,
lng: number | undefined
): Promise<string> => {
if (!lat || !lng) {
return 'Address';
}
try {
const response = await fetch(
`https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&key=${process.env.EXPO_PUBLIC_GOOGLE_MAPS_API_KEY}`
);
if (!response.ok) {
throw new Error('Geocoding API request failed');
}
const result = await response.json();
const address = result?.results?.[0]?.formatted_address || result?.plus_code?.compound_code || 'Address';
return address;
} catch (error) {
console.error('Error fetching address:', error);
return 'Address';
}
};


const onLocationChange = async (
location: { latitude: number; longitude: number },
) => {
setWebMapRegion({
latitude: location.latitude,
longitude: location.longitude,
});

const address = await fetchMapRegionAddress(
location.latitude,
location.longitude
);

if (onFormattedAddressChange) {
onFormattedAddressChange(address as string);
}
};

const onMapLocationChange = (region: any) => {
const onMapLocationChange = async (region: any) => {
onMapRegionChange({
latitude: region.latitude,
longitude: region.longitude,
});

const address = await fetchMapRegionAddress(
region.latitude,
region.longitude
);

if (onFormattedAddressChange) {
onFormattedAddressChange(address as string);
}
}

useEffect(() => {
Expand Down
Loading