import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
FlatList,
Platform,
StyleSheet,
View,
useWindowDimensions,
} from "react-native";
import PersonaCard from "../cards/PersonaCard/PersonaCard";
import { ai } from "../../assets";
import { getCreationStageStates } from "../../utils/projectStages";
const STAGE_CARD_CONTENT = [
{
key: "songwriter",
title: "Nathalie",
description: "Let’s write lyrics together !",
image: ai.leftIcon,
},
{
key: "beatmaker",
title: "Theo",
description: "Come back, when you'll have lyrics!",
image: ai.rightIcon,
},
{
key: "designer",
title: "Bena",
description: "Come back when you want to generate cover!",
image: ai.leftIcon,
},
{
key: "director",
title: "John",
description: "Come back when your audio is ready!",
image: ai.rightIcon,
},
];
const FeatureCarousel = ({
style,
selectedProject,
stageStates: stageStatesProp,
activeIndex,
onActiveIndexChange,
}) => {
const stageStates = useMemo(() => {
if (stageStatesProp) {
return stageStatesProp;
}
return getCreationStageStates(selectedProject);
}, [stageStatesProp, selectedProject]);
const carouselItems = useMemo(
() =>
STAGE_CARD_CONTENT.map((item, index) => ({
...item,
isLocked: stageStates[index]?.isLocked ?? true,
})),
[stageStates],
);
const { height: windowHeight } = useWindowDimensions();
const isWeb = Platform.OS === "web";
const [viewportHeight, setViewportHeight] = useState(() =>
Math.max(windowHeight, 1),
);
const updateSnapHeight = useCallback((height) => {
if (!height || Number.isNaN(height)) {
return;
}
setViewportHeight((prev) => {
if (prev == null || Math.abs(prev - height) > 0.5) {
return height;
}
return prev;
});
}, []);
useEffect(() => {
updateSnapHeight(Math.max(windowHeight, 1));
}, [updateSnapHeight, windowHeight]);
const itemHeight = Math.max(viewportHeight, 1);
const listRef = useRef(null);
const pendingScrollRef = useRef(false);
const alignTimeoutRef = useRef(null);
const activeIndexRef = useRef(
typeof activeIndex === "number" ? activeIndex : 0,
);
const onActiveIndexChangeRef = useRef(onActiveIndexChange);
useEffect(() => {
onActiveIndexChangeRef.current = onActiveIndexChange;
}, [onActiveIndexChange]);
useEffect(() => {
if (typeof activeIndex === "number") {
activeIndexRef.current = activeIndex;
}
}, [activeIndex]);
const clampIndex = useCallback(
(index) => {
if (!carouselItems.length) {
return 0;
}
if (index < 0) {
return 0;
}
if (index >= carouselItems.length) {
return carouselItems.length - 1;
}
return index;
},
[carouselItems.length],
);
const clearPendingAlignment = useCallback(() => {
if (alignTimeoutRef.current != null) {
clearTimeout(alignTimeoutRef.current);
alignTimeoutRef.current = null;
}
}, []);
useEffect(() => () => clearPendingAlignment(), [clearPendingAlignment]);
const scrollToIndex = useCallback(
(index, animated = true, heightOverride) => {
const ref = listRef.current;
if (!ref) {
return;
}
const clamped = clampIndex(index);
const height =
heightOverride && heightOverride > 0 ? heightOverride : itemHeight;
if (isWeb) {
if (!height) {
return;
}
updateSnapHeight(height);
try {
ref.scrollToOffset({ offset: clamped * height, animated: false });
} catch (_error) {
// Ignore scroll errors when list is not ready yet.
}
activeIndexRef.current = clamped;
return;
}
try {
pendingScrollRef.current = !!animated;
ref.scrollToIndex({ index: clamped, animated });
activeIndexRef.current = clamped;
} catch (_error) {
pendingScrollRef.current = false;
}
},
[clampIndex, isWeb, itemHeight, updateSnapHeight],
);
useEffect(() => {
if (
listRef.current == null ||
typeof activeIndex !== "number" ||
activeIndex < 0 ||
activeIndex >= carouselItems.length ||
(isWeb && !itemHeight)
) {
return;
}
scrollToIndex(activeIndex);
}, [activeIndex, carouselItems.length, isWeb, itemHeight, scrollToIndex]);
useEffect(() => {
if (listRef.current == null || (isWeb && !itemHeight)) {
return;
}
scrollToIndex(activeIndexRef.current, false);
}, [carouselItems.length, isWeb, itemHeight, scrollToIndex]);
const alignToOffset = useCallback(
(offset, layoutHeight) => {
const height =
layoutHeight && layoutHeight > 0 ? layoutHeight : itemHeight;
if (!height) {
return;
}
updateSnapHeight(height);
const nextIndex = clampIndex(Math.round(offset / height));
const hasChanged = nextIndex !== activeIndexRef.current;
if (hasChanged) {
activeIndexRef.current = nextIndex;
const callback = onActiveIndexChangeRef.current;
if (callback) {
callback(nextIndex);
}
}
if (isWeb || hasChanged) {
scrollToIndex(nextIndex, !isWeb, height);
}
},
[clampIndex, isWeb, itemHeight, scrollToIndex, updateSnapHeight],
);
const handleScrollEnd = useCallback(
(event) => {
clearPendingAlignment();
const offsetY = event?.nativeEvent?.contentOffset?.y ?? 0;
const layoutHeight = event?.nativeEvent?.layoutMeasurement?.height;
alignToOffset(offsetY, layoutHeight);
},
[alignToOffset, clearPendingAlignment],
);
const handleScroll = useCallback(
(event) => {
if (!isWeb) {
return;
}
const offsetY = event?.nativeEvent?.contentOffset?.y ?? 0;
const layoutHeight = event?.nativeEvent?.layoutMeasurement?.height;
clearPendingAlignment();
alignTimeoutRef.current = setTimeout(() => {
alignToOffset(offsetY, layoutHeight);
alignTimeoutRef.current = null;
}, 80);
},
[alignToOffset, clearPendingAlignment, isWeb],
);
const handleLayout = useCallback(
(event) => {
const layoutHeight = event?.nativeEvent?.layout?.height ?? 0;
if (layoutHeight > 0) {
updateSnapHeight(layoutHeight);
}
},
[updateSnapHeight],
);
const keyExtractor = useCallback((item) => item.key, []);
const renderItem = useCallback(
({ item, index }) => (
),
[itemHeight],
);
const getItemLayout = useCallback(
(_data, index) => ({
length: itemHeight,
offset: itemHeight * index,
index,
}),
[itemHeight],
);
const viewabilityConfig = useRef({ viewAreaCoveragePercentThreshold: 60 });
const handleViewableItemsChangedRef = useRef();
if (!handleViewableItemsChangedRef.current) {
handleViewableItemsChangedRef.current = ({ viewableItems }) => {
if (!viewableItems?.length) return;
const firstVisible = viewableItems.find((item) => item?.isViewable);
if (!firstVisible || firstVisible.index == null) return;
if (pendingScrollRef.current) {
if (firstVisible.index === activeIndexRef.current) {
pendingScrollRef.current = false;
}
return;
}
const callback = onActiveIndexChangeRef.current;
if (callback && firstVisible.index !== activeIndexRef.current) {
callback(firstVisible.index);
}
};
}
const snapOffsets = useMemo(() => {
if (!itemHeight || !isWeb) {
return undefined;
}
return carouselItems.map((_, index) => index * itemHeight);
}, [carouselItems.length, isWeb, itemHeight]);
return (
);
};
export default FeatureCarousel;
const styles = StyleSheet.create({
container: {
flex: 1,
width: "100%",
},
list: {
flex: 1,
},
});