animation, dots, dropdown, popover and design fixes

This commit is contained in:
Thomas Demirdjian
2025-10-06 10:13:17 +02:00
parent 3d14e47770
commit bd1ce3d26c
7 changed files with 427 additions and 116 deletions
@@ -6,6 +6,7 @@ import React, {
useState,
} from "react";
import {
Animated,
FlatList,
Platform,
StyleSheet,
@@ -15,6 +16,8 @@ import {
import { ai } from "../../assets";
import { getCreationStageStates } from "../../utils/projectStages";
import PersonaCard from "../cards/PersonaCard/PersonaCard";
import AnimatedPaginationDot from "../AnimatedPaginationDot/AnimatedPaginationDot";
import { BlurView } from "expo-blur";
const STAGE_CARD_CONTENT = [
{
@@ -65,14 +68,14 @@ const FeatureCarousel = ({
...item,
isLocked: stageStates[index]?.isLocked ?? true,
})),
[stageStates]
[stageStates],
);
const { height: windowHeight } = useWindowDimensions();
const isWeb = Platform.OS === "web";
const [viewportHeight, setViewportHeight] = useState(() =>
Math.max(windowHeight, 1)
Math.max(windowHeight, 1),
);
const updateSnapHeight = useCallback((height) => {
@@ -97,9 +100,10 @@ const FeatureCarousel = ({
const pendingScrollRef = useRef(false);
const alignTimeoutRef = useRef(null);
const activeIndexRef = useRef(
typeof activeIndex === "number" ? activeIndex : 0
typeof activeIndex === "number" ? activeIndex : 0,
);
const onActiveIndexChangeRef = useRef(onActiveIndexChange);
const scrollY = useRef(new Animated.Value(0)).current;
useEffect(() => {
onActiveIndexChangeRef.current = onActiveIndexChange;
@@ -124,7 +128,7 @@ const FeatureCarousel = ({
}
return index;
},
[carouselItems.length]
[carouselItems.length],
);
const clearPendingAlignment = useCallback(() => {
@@ -169,7 +173,7 @@ const FeatureCarousel = ({
pendingScrollRef.current = false;
}
},
[clampIndex, isWeb, itemHeight, updateSnapHeight]
[clampIndex, isWeb, itemHeight, updateSnapHeight],
);
useEffect(() => {
@@ -234,7 +238,7 @@ const FeatureCarousel = ({
scrollToIndex(nextIndex, shouldAnimate, height);
}
},
[clampIndex, isWeb, itemHeight, scrollToIndex, updateSnapHeight]
[clampIndex, isWeb, itemHeight, scrollToIndex, updateSnapHeight],
);
const handleScrollEnd = useCallback(
@@ -244,7 +248,7 @@ const FeatureCarousel = ({
const layoutHeight = event?.nativeEvent?.layoutMeasurement?.height;
alignToOffset(offsetY, layoutHeight);
},
[alignToOffset, clearPendingAlignment]
[alignToOffset, clearPendingAlignment],
);
const handleScroll = useCallback(
@@ -260,7 +264,16 @@ const FeatureCarousel = ({
alignTimeoutRef.current = null;
}, 80);
},
[alignToOffset, clearPendingAlignment, isWeb]
[alignToOffset, clearPendingAlignment, isWeb],
);
const animatedScrollHandler = useMemo(
() =>
Animated.event([{ nativeEvent: { contentOffset: { y: scrollY } } }], {
useNativeDriver: false,
listener: isWeb ? handleScroll : undefined,
}),
[handleScroll, isWeb, scrollY],
);
const handleLayout = useCallback(
@@ -270,7 +283,7 @@ const FeatureCarousel = ({
updateSnapHeight(layoutHeight);
}
},
[updateSnapHeight]
[updateSnapHeight],
);
const keyExtractor = useCallback((item) => item.key, []);
@@ -284,7 +297,7 @@ const FeatureCarousel = ({
height={itemHeight}
/>
),
[itemHeight]
[itemHeight],
);
const getItemLayout = useCallback(
@@ -293,7 +306,7 @@ const FeatureCarousel = ({
offset: itemHeight * index,
index,
}),
[itemHeight]
[itemHeight],
);
const viewabilityConfig = useRef({ viewAreaCoveragePercentThreshold: 60 });
@@ -324,8 +337,20 @@ const FeatureCarousel = ({
return carouselItems.map((_, index) => index * itemHeight);
}, [carouselItems, isWeb, itemHeight]);
const blurIntensity = isWeb ? 80 : 30;
const dotsWrapperStyle = useMemo(
() => [
styles.dotsWrapperBase,
isWeb ? styles.dotsWrapperWeb : styles.dotsWrapperNative,
],
[isWeb],
);
return (
<View style={[styles.container, style]} onLayout={handleLayout}>
<View
style={[styles.container, isWeb && styles.containerWeb, style]}
onLayout={handleLayout}
>
<FlatList
ref={listRef}
data={carouselItems}
@@ -348,10 +373,28 @@ const FeatureCarousel = ({
disableIntervalMomentum={!isWeb}
decelerationRate={!isWeb ? "fast" : undefined}
style={styles.list}
onScroll={isWeb ? handleScroll : undefined}
onScroll={animatedScrollHandler}
onMomentumScrollEnd={handleScrollEnd}
onScrollEndDrag={isWeb ? handleScrollEnd : undefined}
/>
<BlurView
intensity={blurIntensity}
tint={Platform.OS === "web" ? undefined : "dark"}
style={dotsWrapperStyle}
pointerEvents="none"
>
<AnimatedPaginationDot
data={carouselItems}
scrollValue={scrollY}
itemDimension={itemHeight}
orientation={AnimatedPaginationDot.orientation.VERTICAL}
expandingDotSize={24}
inactiveDotOpacity={0.3}
containerStyle={styles.dotsContainer}
dotStyle={styles.dot}
baseDotSize={8}
/>
</BlurView>
</View>
);
};
@@ -362,8 +405,44 @@ const styles = StyleSheet.create({
container: {
flex: 1,
width: "100%",
flexDirection: "row",
},
containerWeb: {
paddingRight: 56,
},
list: {
flex: 1,
},
dotsWrapperBase: {
justifyContent: "center",
alignItems: "center",
borderRadius: 16,
paddingVertical: 12,
paddingHorizontal: 8,
overflow: "hidden",
backgroundColor: "rgba(18, 18, 18, 0.2)",
},
dotsWrapperWeb: {
position: "absolute",
right: 12,
top: 0,
bottom: 0,
maxHeight: "75%",
alignSelf: "center",
},
dotsWrapperNative: {
marginLeft: 12,
alignSelf: "center",
},
dotsContainer: {
flexDirection: "column",
},
dot: {
width: 8,
height: 8,
marginHorizontal: 0,
marginVertical: 6,
borderRadius: 999,
backgroundColor: "rgba(255,255,255,0.4)",
},
});