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
124 changes: 25 additions & 99 deletions components/InfluencerCard.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
import { router } from "expo-router";
import React, { useState, useRef } from "react";
import React, { useEffect, useState } from "react";
import {
View,
Text,
Image,
StyleSheet,
TouchableOpacity,
Dimensions,
Modal,
Platform,
ActivityIndicator,
} from "react-native";
import { Card, Avatar, IconButton, Chip } from "react-native-paper";
import Carousel from "react-native-reanimated-carousel";
import { ResizeMode, Video } from "expo-av";
import { stylesFn } from "@/styles/InfluencerCard.styles";
import { useTheme } from "@react-navigation/native";
import {
PinchGestureHandler,
PinchGestureHandlerGestureEvent,
TapGestureHandler,
State,
} from "react-native-gesture-handler";
import Animated, {
useAnimatedGestureHandler,
Expand All @@ -29,7 +24,7 @@ import Animated, {
} from "react-native-reanimated";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import Colors from "@/constants/Colors";
import { influencers } from "@/constants/Influencers";
import CarouselNative from "./ui/carousel/carousel";

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

Expand Down Expand Up @@ -57,19 +52,15 @@ interface InfluencerCardPropsType {

const InfluencerCard = (props: InfluencerCardPropsType) => {
const [bioExpanded, setBioExpanded] = useState(false);
const [currentIndex, setCurrentIndex] = useState(0);
const [muted, setMuted] = useState(true);
const [isPlaying, setIsPlaying] = useState(true);
const [isZoomed, setIsZoomed] = useState(false);
const [selectedImage, setSelectedImage] = useState<string | null>(null);
const [isSwiping, setIsSwiping] = useState(false);
const [isZoomed, setIsZoomed] = useState(false);
const [loading, setLoading] = useState(true);

// Animation values for zoom
const scale = useSharedValue(1);
const focalX = useSharedValue(0);
const focalY = useSharedValue(0);

const videoRefs = useRef<{ [key: number]: any }>({});
const influencer = props.influencer;
const theme = useTheme();
const styles = stylesFn(theme);
Expand Down Expand Up @@ -102,69 +93,24 @@ const InfluencerCard = (props: InfluencerCardPropsType) => {
};
});

const handleIndexChange = (index: number) => {
setCurrentIndex(index);
setIsSwiping(true);
// Reset swiping state after a short delay
setTimeout(() => setIsSwiping(false), 150);
Object.values(videoRefs.current).forEach((videoRef) => {
if (videoRef && videoRef.seek) {
videoRef.seek(0);
setIsPlaying(false);
}
});
};
const onImagePress = (data: any) => {
setSelectedImage(data); // TODO: data -> uri
setIsZoomed(true);
}
Comment on lines +96 to +99

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 onImagePress handler.

The any type and TODO comment suggest room for improvement:

  1. The parameter type should be more specific
  2. The TODO comment should be addressed

Consider this improvement:

-  const onImagePress = (data: any) => {
-    setSelectedImage(data); // TODO: data -> uri
+  const onImagePress = (uri: string) => {
+    setSelectedImage(uri);

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


const handleImagePress = (uri: string) => {
if (!isSwiping) {
setSelectedImage(uri);
setIsZoomed(true);
}
};
useEffect(() => {
const timer = setTimeout(() => {
setLoading(false);
}, 1000);

const renderMediaItem = ({ item, index }: any) => {
if (item.type === "image") {
return (
<TapGestureHandler
onHandlerStateChange={({ nativeEvent }) => {
if (nativeEvent.state === State.ACTIVE) {
handleImagePress(item.uri);
}
}}
>
<Animated.View>
<Image
source={{ uri: item.url || item.uri }}
style={styles.media}
/>
</Animated.View>
</TapGestureHandler>
);
} else {
return (
<Video
ref={(ref) => {
if (ref) {
videoRefs.current[index] = ref;
}
}}
source={
item.appleUrl
? {
uri: Platform.OS === "ios" ? item.appleUrl : item.playUrl,
}
: require("../assets/videos/ForBiggerJoyrides.mp4")
}
style={styles.media}
resizeMode={ResizeMode.COVER}
isLooping={false}
shouldPlay={false}
useNativeControls
onError={(error) => console.log("Video Error:", error)}
/>
);
}
};
return () => {
clearTimeout(timer);
};
}, []);
Comment on lines +101 to +109

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 loading state implementation

The current implementation uses a fixed 1-second timeout which might not reflect actual media loading times. This could lead to:

  1. Showing loading spinner even after content is ready
  2. Hiding loading spinner before content is fully loaded

Consider implementing actual loading state tracking:

-  const [loading, setLoading] = useState(true);
+  const [mediaLoading, setMediaLoading] = useState(true);
 
-  useEffect(() => {
-    const timer = setTimeout(() => {
-      setLoading(false);
-    }, 1000);
-
-    return () => {
-      clearTimeout(timer);
-    };
-  }, []);
-
-  if (loading) {
+  if (mediaLoading) {
     return <ActivityIndicator />;
   }

Then pass the loading state handler to CarouselNative:

   <CarouselNative
     data={influencer.media}
     onImagePress={onImagePress}
+    onLoadComplete={() => setMediaLoading(false)}
   />

Also applies to: 111-113


if (loading) {
return <ActivityIndicator />;
}

// Rest of the component remains the same...
return (
Expand Down Expand Up @@ -199,30 +145,10 @@ const InfluencerCard = (props: InfluencerCardPropsType) => {
/>
</View>

<View style={styles.carouselContainer}>
<Carousel
loop
width={Dimensions.get("window").width - 45}
height={250}
data={influencer.media}
renderItem={renderMediaItem}
onSnapToItem={handleIndexChange}
style={{
padding: 10,
}}
/>
<View style={styles.indicatorContainer}>
{influencer.media.map((_: any, index: any) => (
<View
key={index}
style={[
styles.indicatorDot,
currentIndex === index && styles.activeDot,
]}
/>
))}
</View>
</View>
<CarouselNative
data={influencer.media}
onImagePress={onImagePress}
/>

<View style={styles.stats}>
<View style={styles.statItem}>
Expand Down
106 changes: 52 additions & 54 deletions components/drawer-layout/DrawerMenuContent.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Text, View } from "@/components/theme/Themed";
import { DrawerContentScrollView } from "@react-navigation/drawer";
import { APP_NAME } from "@/constants/App";
import DrawerMenuItem from "./DrawerMenuItem";
import { useBreakpoints } from "@/hooks";
Expand All @@ -9,7 +8,7 @@ import { useNavigation, useRouter } from "expo-router";
import { useBrandContext } from "@/contexts/brand-context.provider";
import { Brand } from "@/types/Brand";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Platform } from "react-native";
import { Platform, ScrollView } from "react-native";
import BrandActionItem from "./BrandActionItem";
import Colors from "@/constants/Colors";
import { useEffect, useState } from "react";
Expand Down Expand Up @@ -81,64 +80,63 @@ const DrawerMenuContent: React.FC<DrawerMenuContentProps> = () => {
<View
style={{
flex: 1,
paddingTop: Platform.OS === 'web' ? 12 : 64,
}}
>
<DrawerContentScrollView>
<View
<View>
<Text
style={{
flex: 1,
gap: 6,
paddingHorizontal: 24,
paddingTop: 8,
paddingBottom: 16,
fontSize: 24,
fontWeight: "bold",
}}
>
<View>
<Text
style={{
paddingHorizontal: 24,
paddingTop: 8,
paddingBottom: 16,
fontSize: 24,
fontWeight: "bold",
}}
>
{APP_NAME}
</Text>
</View>
<View>
{
!xl && (
<Searchbar
onChangeText={handleSearchChange}
placeholder="Search"
value={search}
style={[
{
marginHorizontal: 14,
marginBottom: 8,
backgroundColor: Colors(theme).platinum
},
]}
/>
)
}
{xl ? DRAWER_MENU_CONTENT_ITEMS.map((tab, index) => (
<DrawerMenuItem
key={index}
tab={tab}
/>
)) : filteredBrands.map((brand) => (
<BrandItem
active={selectedBrand?.id === brand.id}
image={brand.image}
key={brand.id.toString()}
menu={true}
onPress={() => handleBrandChange(brand)}
showImage={true}
title={brand.name}
/>
))}
</View>
{APP_NAME}
</Text>
{
!xl && (
<Searchbar
onChangeText={handleSearchChange}
placeholder="Search"
value={search}
style={[
{
marginHorizontal: 14,
marginBottom: 8,
backgroundColor: Colors(theme).platinum
},
]}
/>
)
}
</View>
<ScrollView
style={{
flex: 1,
gap: 6,
}}
>
<View>
{xl ? DRAWER_MENU_CONTENT_ITEMS.map((tab, index) => (
<DrawerMenuItem
key={index}
tab={tab}
/>
)) : filteredBrands.map((brand) => (
<BrandItem
active={selectedBrand?.id === brand.id}
image={brand.image}
key={brand.id.toString()}
menu={true}
onPress={() => handleBrandChange(brand)}
showImage={true}
title={brand.name}
/>
))}
</View>
</DrawerContentScrollView>
</ScrollView>
<View
style={{
marginBottom: bottom + Platform.OS === 'android' ? 24 : 44,
Expand Down
87 changes: 87 additions & 0 deletions components/ui/carousel/carousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { useRef, useState } from "react";
import {
Dimensions,
} from "react-native";
import Carousel from "react-native-reanimated-carousel";

import { View } from "@/components/theme/Themed";
import { stylesFn } from "@/styles/InfluencerCard.styles";
import { useTheme } from "@react-navigation/native";
import RenderMediaItem from "./render-media-item";

interface CarouselProps {
data: any[]; // TODO: Add specific type
onImagePress?: (data: any) => void;
}
Comment on lines +12 to +15

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

Replace any type with a proper interface definition.

The any type reduces type safety. Based on the component's usage, consider defining a proper type:

interface MediaItem {
  uri: string;
  type: 'image' | 'video';
  thumbnail?: string;
}

interface CarouselProps {
  data: MediaItem[];
  onImagePress?: (uri: string) => void;
}


const CarouselNative: React.FC<CarouselProps> = ({
data,
onImagePress,
}) => {
const [currentIndex, setCurrentIndex] = useState(0);
const [isPlaying, setIsPlaying] = useState(true);
const [isSwiping, setIsSwiping] = useState(false);

const videoRefs = useRef<{ [key: number]: any }>({});

const theme = useTheme();
const styles = stylesFn(theme);

const handleIndexChange = (index: number) => {
setCurrentIndex(index);
setIsSwiping(true);
// Reset swiping state after a short delay
setTimeout(() => setIsSwiping(false), 150);
Object.values(videoRefs.current).forEach((videoRef) => {
if (videoRef && videoRef.seek) {
videoRef.seek(0);
setIsPlaying(false);
}
});
};
Comment on lines +35 to +41

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 video ref handling with optional chaining and cleanup.

The video ref access could be safer and should be cleaned up on unmount:

// Inside component:
useEffect(() => {
  return () => {
    // Cleanup video refs on unmount
    Object.values(videoRefs.current).forEach((videoRef) => {
      videoRef?.seek?.(0);
    });
  };
}, []);

// In handleIndexChange:
Object.values(videoRefs.current).forEach((videoRef) => {
  videoRef?.seek?.(0);
  setIsPlaying(false);
});
🧰 Tools
🪛 Biome

[error] 36-36: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


const handleImagePress = (uri: string) => {
if (!isSwiping) {
if (onImagePress) {
onImagePress(uri);
}
}
};

return (
<View style={styles.carouselContainer}>
<Carousel
loop
width={Dimensions.get("window").width - 45}
height={250}
data={data}
renderItem={({ item, index }) => (
<RenderMediaItem
handleImagePress={handleImagePress}
index={index}
item={item}
videoRefs={videoRefs}
/>
)}
onSnapToItem={handleIndexChange}
style={{
padding: 10,
}}
/>

<View style={styles.indicatorContainer}>
{data.map((_: any, index: any) => (
<View
key={index}
style={[
styles.indicatorDot,
currentIndex === index && styles.activeDot,
]}
/>
))}
</View>
</View>
);
};

export default CarouselNative;
Loading