animation, dots, dropdown, popover and design fixes
This commit is contained in:
@@ -0,0 +1,136 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { Animated, StyleSheet, useWindowDimensions, View } from "react-native";
|
||||||
|
import { Palette } from "../../styles";
|
||||||
|
|
||||||
|
const ORIENTATION = {
|
||||||
|
HORIZONTAL: "horizontal",
|
||||||
|
VERTICAL: "vertical",
|
||||||
|
};
|
||||||
|
|
||||||
|
const AnimatedPaginationDot = ({
|
||||||
|
data = [],
|
||||||
|
scrollValue,
|
||||||
|
itemDimension,
|
||||||
|
containerStyle,
|
||||||
|
dotStyle,
|
||||||
|
orientation = ORIENTATION.HORIZONTAL,
|
||||||
|
expandingDotSize = 20,
|
||||||
|
inactiveDotOpacity = 0.4,
|
||||||
|
inactiveDotColor = "rgba(255,255,255,0.4)",
|
||||||
|
activeDotColor = Palette.white,
|
||||||
|
baseDotSize = 10,
|
||||||
|
}) => {
|
||||||
|
const animatedValue = useMemo(
|
||||||
|
() => scrollValue || new Animated.Value(0),
|
||||||
|
[scrollValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { width, height } = useWindowDimensions();
|
||||||
|
const distance = useMemo(() => {
|
||||||
|
if (typeof itemDimension === "number" && itemDimension > 0) {
|
||||||
|
return itemDimension;
|
||||||
|
}
|
||||||
|
if (orientation === ORIENTATION.VERTICAL) {
|
||||||
|
return Math.max(height, 1);
|
||||||
|
}
|
||||||
|
return Math.max(width, 1);
|
||||||
|
}, [height, itemDimension, orientation, width]);
|
||||||
|
|
||||||
|
const resolvedDotStyle = useMemo(() => StyleSheet.flatten(dotStyle) || {}, [dotStyle]);
|
||||||
|
const defaultSize = Math.max(baseDotSize, 1);
|
||||||
|
const baseWidth =
|
||||||
|
typeof resolvedDotStyle?.width === "number"
|
||||||
|
? resolvedDotStyle.width
|
||||||
|
: defaultSize;
|
||||||
|
const baseHeight =
|
||||||
|
typeof resolvedDotStyle?.height === "number"
|
||||||
|
? resolvedDotStyle.height
|
||||||
|
: defaultSize;
|
||||||
|
|
||||||
|
const containerOrientationStyle =
|
||||||
|
orientation === ORIENTATION.VERTICAL
|
||||||
|
? styles.containerVertical
|
||||||
|
: styles.containerHorizontal;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
pointerEvents="none"
|
||||||
|
style={[styles.containerBase, containerOrientationStyle, containerStyle]}
|
||||||
|
>
|
||||||
|
{data.map((_, index) => {
|
||||||
|
const inputRange = [
|
||||||
|
(index - 1) * distance,
|
||||||
|
index * distance,
|
||||||
|
(index + 1) * distance,
|
||||||
|
];
|
||||||
|
|
||||||
|
const staticSizeStyle = {
|
||||||
|
width: baseWidth,
|
||||||
|
height: baseHeight,
|
||||||
|
};
|
||||||
|
|
||||||
|
const color = animatedValue.interpolate({
|
||||||
|
inputRange,
|
||||||
|
outputRange: [inactiveDotColor, activeDotColor, inactiveDotColor],
|
||||||
|
extrapolate: "clamp",
|
||||||
|
});
|
||||||
|
|
||||||
|
const opacity = animatedValue.interpolate({
|
||||||
|
inputRange,
|
||||||
|
outputRange: [inactiveDotOpacity, 1, inactiveDotOpacity],
|
||||||
|
extrapolate: "clamp",
|
||||||
|
});
|
||||||
|
|
||||||
|
const primarySize = animatedValue.interpolate({
|
||||||
|
inputRange,
|
||||||
|
outputRange: [
|
||||||
|
orientation === ORIENTATION.VERTICAL ? baseHeight : baseWidth,
|
||||||
|
expandingDotSize,
|
||||||
|
orientation === ORIENTATION.VERTICAL ? baseHeight : baseWidth,
|
||||||
|
],
|
||||||
|
extrapolate: "clamp",
|
||||||
|
});
|
||||||
|
|
||||||
|
const animatedSizeStyle =
|
||||||
|
orientation === ORIENTATION.VERTICAL
|
||||||
|
? { height: primarySize }
|
||||||
|
: { width: primarySize };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
key={`pagination-dot-${index}`}
|
||||||
|
style={[
|
||||||
|
styles.dotBase,
|
||||||
|
staticSizeStyle,
|
||||||
|
dotStyle,
|
||||||
|
animatedSizeStyle,
|
||||||
|
{ backgroundColor: color, opacity },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AnimatedPaginationDot.orientation = ORIENTATION;
|
||||||
|
|
||||||
|
export default AnimatedPaginationDot;
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
containerBase: {
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
containerHorizontal: {
|
||||||
|
flexDirection: "row",
|
||||||
|
},
|
||||||
|
containerVertical: {
|
||||||
|
flexDirection: "column",
|
||||||
|
},
|
||||||
|
dotBase: {
|
||||||
|
borderRadius: 999,
|
||||||
|
marginHorizontal: 4,
|
||||||
|
marginVertical: 4,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -6,6 +6,7 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
|
Animated,
|
||||||
FlatList,
|
FlatList,
|
||||||
Platform,
|
Platform,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -15,6 +16,8 @@ import {
|
|||||||
import { ai } from "../../assets";
|
import { ai } from "../../assets";
|
||||||
import { getCreationStageStates } from "../../utils/projectStages";
|
import { getCreationStageStates } from "../../utils/projectStages";
|
||||||
import PersonaCard from "../cards/PersonaCard/PersonaCard";
|
import PersonaCard from "../cards/PersonaCard/PersonaCard";
|
||||||
|
import AnimatedPaginationDot from "../AnimatedPaginationDot/AnimatedPaginationDot";
|
||||||
|
import { BlurView } from "expo-blur";
|
||||||
|
|
||||||
const STAGE_CARD_CONTENT = [
|
const STAGE_CARD_CONTENT = [
|
||||||
{
|
{
|
||||||
@@ -65,14 +68,14 @@ const FeatureCarousel = ({
|
|||||||
...item,
|
...item,
|
||||||
isLocked: stageStates[index]?.isLocked ?? true,
|
isLocked: stageStates[index]?.isLocked ?? true,
|
||||||
})),
|
})),
|
||||||
[stageStates]
|
[stageStates],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { height: windowHeight } = useWindowDimensions();
|
const { height: windowHeight } = useWindowDimensions();
|
||||||
const isWeb = Platform.OS === "web";
|
const isWeb = Platform.OS === "web";
|
||||||
|
|
||||||
const [viewportHeight, setViewportHeight] = useState(() =>
|
const [viewportHeight, setViewportHeight] = useState(() =>
|
||||||
Math.max(windowHeight, 1)
|
Math.max(windowHeight, 1),
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateSnapHeight = useCallback((height) => {
|
const updateSnapHeight = useCallback((height) => {
|
||||||
@@ -97,9 +100,10 @@ const FeatureCarousel = ({
|
|||||||
const pendingScrollRef = useRef(false);
|
const pendingScrollRef = useRef(false);
|
||||||
const alignTimeoutRef = useRef(null);
|
const alignTimeoutRef = useRef(null);
|
||||||
const activeIndexRef = useRef(
|
const activeIndexRef = useRef(
|
||||||
typeof activeIndex === "number" ? activeIndex : 0
|
typeof activeIndex === "number" ? activeIndex : 0,
|
||||||
);
|
);
|
||||||
const onActiveIndexChangeRef = useRef(onActiveIndexChange);
|
const onActiveIndexChangeRef = useRef(onActiveIndexChange);
|
||||||
|
const scrollY = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onActiveIndexChangeRef.current = onActiveIndexChange;
|
onActiveIndexChangeRef.current = onActiveIndexChange;
|
||||||
@@ -124,7 +128,7 @@ const FeatureCarousel = ({
|
|||||||
}
|
}
|
||||||
return index;
|
return index;
|
||||||
},
|
},
|
||||||
[carouselItems.length]
|
[carouselItems.length],
|
||||||
);
|
);
|
||||||
|
|
||||||
const clearPendingAlignment = useCallback(() => {
|
const clearPendingAlignment = useCallback(() => {
|
||||||
@@ -169,7 +173,7 @@ const FeatureCarousel = ({
|
|||||||
pendingScrollRef.current = false;
|
pendingScrollRef.current = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[clampIndex, isWeb, itemHeight, updateSnapHeight]
|
[clampIndex, isWeb, itemHeight, updateSnapHeight],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -234,7 +238,7 @@ const FeatureCarousel = ({
|
|||||||
scrollToIndex(nextIndex, shouldAnimate, height);
|
scrollToIndex(nextIndex, shouldAnimate, height);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[clampIndex, isWeb, itemHeight, scrollToIndex, updateSnapHeight]
|
[clampIndex, isWeb, itemHeight, scrollToIndex, updateSnapHeight],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleScrollEnd = useCallback(
|
const handleScrollEnd = useCallback(
|
||||||
@@ -244,7 +248,7 @@ const FeatureCarousel = ({
|
|||||||
const layoutHeight = event?.nativeEvent?.layoutMeasurement?.height;
|
const layoutHeight = event?.nativeEvent?.layoutMeasurement?.height;
|
||||||
alignToOffset(offsetY, layoutHeight);
|
alignToOffset(offsetY, layoutHeight);
|
||||||
},
|
},
|
||||||
[alignToOffset, clearPendingAlignment]
|
[alignToOffset, clearPendingAlignment],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleScroll = useCallback(
|
const handleScroll = useCallback(
|
||||||
@@ -260,7 +264,16 @@ const FeatureCarousel = ({
|
|||||||
alignTimeoutRef.current = null;
|
alignTimeoutRef.current = null;
|
||||||
}, 80);
|
}, 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(
|
const handleLayout = useCallback(
|
||||||
@@ -270,7 +283,7 @@ const FeatureCarousel = ({
|
|||||||
updateSnapHeight(layoutHeight);
|
updateSnapHeight(layoutHeight);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[updateSnapHeight]
|
[updateSnapHeight],
|
||||||
);
|
);
|
||||||
|
|
||||||
const keyExtractor = useCallback((item) => item.key, []);
|
const keyExtractor = useCallback((item) => item.key, []);
|
||||||
@@ -284,7 +297,7 @@ const FeatureCarousel = ({
|
|||||||
height={itemHeight}
|
height={itemHeight}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[itemHeight]
|
[itemHeight],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getItemLayout = useCallback(
|
const getItemLayout = useCallback(
|
||||||
@@ -293,7 +306,7 @@ const FeatureCarousel = ({
|
|||||||
offset: itemHeight * index,
|
offset: itemHeight * index,
|
||||||
index,
|
index,
|
||||||
}),
|
}),
|
||||||
[itemHeight]
|
[itemHeight],
|
||||||
);
|
);
|
||||||
|
|
||||||
const viewabilityConfig = useRef({ viewAreaCoveragePercentThreshold: 60 });
|
const viewabilityConfig = useRef({ viewAreaCoveragePercentThreshold: 60 });
|
||||||
@@ -324,8 +337,20 @@ const FeatureCarousel = ({
|
|||||||
return carouselItems.map((_, index) => index * itemHeight);
|
return carouselItems.map((_, index) => index * itemHeight);
|
||||||
}, [carouselItems, isWeb, itemHeight]);
|
}, [carouselItems, isWeb, itemHeight]);
|
||||||
|
|
||||||
|
const blurIntensity = isWeb ? 80 : 30;
|
||||||
|
const dotsWrapperStyle = useMemo(
|
||||||
|
() => [
|
||||||
|
styles.dotsWrapperBase,
|
||||||
|
isWeb ? styles.dotsWrapperWeb : styles.dotsWrapperNative,
|
||||||
|
],
|
||||||
|
[isWeb],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, style]} onLayout={handleLayout}>
|
<View
|
||||||
|
style={[styles.container, isWeb && styles.containerWeb, style]}
|
||||||
|
onLayout={handleLayout}
|
||||||
|
>
|
||||||
<FlatList
|
<FlatList
|
||||||
ref={listRef}
|
ref={listRef}
|
||||||
data={carouselItems}
|
data={carouselItems}
|
||||||
@@ -348,10 +373,28 @@ const FeatureCarousel = ({
|
|||||||
disableIntervalMomentum={!isWeb}
|
disableIntervalMomentum={!isWeb}
|
||||||
decelerationRate={!isWeb ? "fast" : undefined}
|
decelerationRate={!isWeb ? "fast" : undefined}
|
||||||
style={styles.list}
|
style={styles.list}
|
||||||
onScroll={isWeb ? handleScroll : undefined}
|
onScroll={animatedScrollHandler}
|
||||||
onMomentumScrollEnd={handleScrollEnd}
|
onMomentumScrollEnd={handleScrollEnd}
|
||||||
onScrollEndDrag={isWeb ? handleScrollEnd : undefined}
|
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>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -362,8 +405,44 @@ const styles = StyleSheet.create({
|
|||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
flexDirection: "row",
|
||||||
|
},
|
||||||
|
containerWeb: {
|
||||||
|
paddingRight: 56,
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
flex: 1,
|
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)",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+44
-68
@@ -9,6 +9,24 @@ import { Palette } from "../styles";
|
|||||||
import { FONT_FAMILY } from "../styles/Fonts";
|
import { FONT_FAMILY } from "../styles/Fonts";
|
||||||
import { Portal } from "@gorhom/portal";
|
import { Portal } from "@gorhom/portal";
|
||||||
|
|
||||||
|
const MoreBtn = ({ onPress, children }) => (
|
||||||
|
<Pressable onPress={onPress}>
|
||||||
|
<BlurView
|
||||||
|
intensity={Platform.OS !== "ios" ? 70 : 30}
|
||||||
|
tint="dark"
|
||||||
|
style={{
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 10,
|
||||||
|
backgroundColor: Palette.glass,
|
||||||
|
borderRadius: 12,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</BlurView>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
|
||||||
const MoreMenu = ({
|
const MoreMenu = ({
|
||||||
visible = false,
|
visible = false,
|
||||||
top = 0,
|
top = 0,
|
||||||
@@ -26,8 +44,8 @@ const MoreMenu = ({
|
|||||||
typeof position?.top === "number"
|
typeof position?.top === "number"
|
||||||
? position.top
|
? position.top
|
||||||
: typeof top === "number"
|
: typeof top === "number"
|
||||||
? top
|
? top
|
||||||
: 0;
|
: 0;
|
||||||
const resolvedRight =
|
const resolvedRight =
|
||||||
typeof position?.right === "number" ? Math.max(position.right, 0) : null;
|
typeof position?.right === "number" ? Math.max(position.right, 0) : null;
|
||||||
const resolvedLeft =
|
const resolvedLeft =
|
||||||
@@ -108,47 +126,7 @@ const MoreMenu = ({
|
|||||||
>
|
>
|
||||||
<View style={{ gap: 2 }}>
|
<View style={{ gap: 2 }}>
|
||||||
{inPlaylist && (
|
{inPlaylist && (
|
||||||
<Pressable onPress={removeFromPlaylist}>
|
<MoreBtn onPress={removeFromPlaylist}>
|
||||||
<BlurView
|
|
||||||
intensity={Platform.OS !== "ios" ? 10 : 20}
|
|
||||||
style={{
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
paddingVertical: 10,
|
|
||||||
backgroundColor: Palette.glass,
|
|
||||||
borderRadius: 12,
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
// experimentalBlurMethod={
|
|
||||||
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
|
||||||
// }
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
fontSize: 14,
|
|
||||||
color: Palette.white,
|
|
||||||
fontFamily: FONT_FAMILY.InterRegular,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Supprimer de la playlist
|
|
||||||
</Text>
|
|
||||||
</BlurView>
|
|
||||||
</Pressable>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Pressable onPress={addToPlaylist}>
|
|
||||||
<BlurView
|
|
||||||
intensity={Platform.OS !== "ios" ? 10 : 20}
|
|
||||||
style={{
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
paddingVertical: 10,
|
|
||||||
backgroundColor: Palette.glass,
|
|
||||||
borderRadius: 12,
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
// experimentalBlurMethod={
|
|
||||||
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
|
||||||
// }
|
|
||||||
>
|
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@@ -156,14 +134,26 @@ const MoreMenu = ({
|
|||||||
fontFamily: FONT_FAMILY.InterRegular,
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Ajouter à une playlist
|
Supprimer de la playlist
|
||||||
</Text>
|
</Text>
|
||||||
</BlurView>
|
</MoreBtn>
|
||||||
</Pressable>
|
)}
|
||||||
|
|
||||||
|
<MoreBtn onPress={addToPlaylist}>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: 14,
|
||||||
|
color: Palette.white,
|
||||||
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Ajouter à une playlist
|
||||||
|
</Text>
|
||||||
|
</MoreBtn>
|
||||||
|
|
||||||
{Array.isArray(extraItems) &&
|
{Array.isArray(extraItems) &&
|
||||||
extraItems.map((item, idx) => (
|
extraItems.map((item, idx) => (
|
||||||
<Pressable
|
<MoreBtn
|
||||||
key={idx}
|
key={idx}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
try {
|
try {
|
||||||
@@ -173,30 +163,16 @@ const MoreMenu = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BlurView
|
<Text
|
||||||
intensity={Platform.OS !== "ios" ? 10 : 20}
|
|
||||||
style={{
|
style={{
|
||||||
paddingHorizontal: 12,
|
fontSize: 14,
|
||||||
paddingVertical: 10,
|
color: Palette.white,
|
||||||
backgroundColor: Palette.glass,
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
borderRadius: 12,
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
}}
|
||||||
// experimentalBlurMethod={
|
|
||||||
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
|
||||||
// }
|
|
||||||
>
|
>
|
||||||
<Text
|
{item?.label || "Action"}
|
||||||
style={{
|
</Text>
|
||||||
fontSize: 14,
|
</MoreBtn>
|
||||||
color: Palette.white,
|
|
||||||
fontFamily: FONT_FAMILY.InterRegular,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item?.label || "Action"}
|
|
||||||
</Text>
|
|
||||||
</BlurView>
|
|
||||||
</Pressable>
|
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Animated,
|
Animated,
|
||||||
|
Dimensions,
|
||||||
Easing,
|
Easing,
|
||||||
FlatList,
|
FlatList,
|
||||||
Image,
|
Image,
|
||||||
@@ -88,9 +89,9 @@ const ProjectDropDown = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleModify = useCallback(
|
const handleModify = useCallback(
|
||||||
(project, topPosition) => {
|
(project, anchor) => {
|
||||||
if (!project?.id) return;
|
if (!project?.id) return;
|
||||||
onModifyProject?.(project, topPosition);
|
onModifyProject?.(project, anchor);
|
||||||
},
|
},
|
||||||
[onModifyProject],
|
[onModifyProject],
|
||||||
);
|
);
|
||||||
@@ -261,23 +262,37 @@ const ProjectRow = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const rowRef = useRef(null);
|
const rowRef = useRef(null);
|
||||||
const [layout, setLayout] = useState(null);
|
const [layout, setLayout] = useState(null);
|
||||||
const [menuPos, setMenuPos] = useState(0);
|
const [menuPos, setMenuPos] = useState({
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
left: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!layout) return;
|
if (!layout) return;
|
||||||
const top = (layout?.y || 0) + 45;
|
const top = (layout?.y || 0) + (layout?.height || 45);
|
||||||
setMenuPos(top);
|
setMenuPos((prev) => ({ ...prev, top }));
|
||||||
}, [layout]);
|
}, [layout]);
|
||||||
|
|
||||||
const onPressMenu = () => {
|
const onPressMenu = () => {
|
||||||
if (!project?.id) return;
|
if (!project?.id) return;
|
||||||
|
|
||||||
|
const updateAnchor = (x = 0, y = 0, width = 0, height = 0) => {
|
||||||
|
const windowWidth = Dimensions.get("window").width || 0;
|
||||||
|
const top = (y || 0) + (height || 0) + 8;
|
||||||
|
const left = Math.max(0, x || 0);
|
||||||
|
const right = Math.max(0, windowWidth - (left + (width || 0)));
|
||||||
|
const anchor = { top, right, left, width, height };
|
||||||
|
setMenuPos(anchor);
|
||||||
|
onModify?.(project, anchor);
|
||||||
|
};
|
||||||
|
|
||||||
if (rowRef?.current?.measureInWindow) {
|
if (rowRef?.current?.measureInWindow) {
|
||||||
try {
|
try {
|
||||||
rowRef.current.measureInWindow((x, y) => {
|
rowRef.current.measureInWindow((x, y, width, height) => {
|
||||||
const top = (y || 0) + 45;
|
updateAnchor(x, y, width, height);
|
||||||
setMenuPos(top);
|
|
||||||
onModify?.(project, top);
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Platform, Text, useWindowDimensions, View } from "react-native";
|
import { Text, useWindowDimensions, View } from "react-native";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { BlurView } from "expo-blur";
|
import { BlurView } from "expo-blur";
|
||||||
import { gutters, Palette } from "../../../styles";
|
import { gutters, Palette } from "../../../styles";
|
||||||
@@ -7,10 +7,10 @@ import { FONT_FAMILY } from "../../../styles/Fonts";
|
|||||||
import FontAwesome from "@expo/vector-icons/FontAwesome";
|
import FontAwesome from "@expo/vector-icons/FontAwesome";
|
||||||
import palette from "../../../styles/Palette";
|
import palette from "../../../styles/Palette";
|
||||||
|
|
||||||
export default function PersonaCard({ item, isLock = false, height }) {
|
export default function PersonaCard({ item, index, isLock = false, height }) {
|
||||||
|
const isImageOnLeft = index % 2 === 0;
|
||||||
const { height: windowHeight } = useWindowDimensions();
|
const { height: windowHeight } = useWindowDimensions();
|
||||||
const baseHeight = Math.max(height ?? windowHeight, 1);
|
const cardHeight = Math.max(height ?? windowHeight, 1);
|
||||||
const cardHeight = Platform.OS === "web" ? Math.round(baseHeight) : baseHeight;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@@ -38,6 +38,8 @@ export default function PersonaCard({ item, isLock = false, height }) {
|
|||||||
tint="dark"
|
tint="dark"
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
|
borderTopLeftRadius: isImageOnLeft ? 20 : 0,
|
||||||
|
borderTopRightRadius: isImageOnLeft ? 0 : 20,
|
||||||
paddingVertical: 20,
|
paddingVertical: 20,
|
||||||
paddingHorizontal: gutters,
|
paddingHorizontal: gutters,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
@@ -74,6 +76,8 @@ export default function PersonaCard({ item, isLock = false, height }) {
|
|||||||
top: 0,
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
|
borderTopLeftRadius: isImageOnLeft ? 20 : 0,
|
||||||
|
borderTopRightRadius: isImageOnLeft ? 0 : 20,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
backgroundColor: "rgba(0,0,0,0.2)",
|
backgroundColor: "rgba(0,0,0,0.2)",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Text, useWindowDimensions, View } from "react-native";
|
import { Text, useWindowDimensions, View, StyleSheet } from "react-native";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { BlurView } from "expo-blur";
|
import { BlurView } from "expo-blur";
|
||||||
import { Palette } from "../../../styles";
|
import { Palette } from "../../../styles";
|
||||||
@@ -44,11 +44,23 @@ export default function PersonaCard({ item, index, isLock = true }) {
|
|||||||
tint="dark"
|
tint="dark"
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
borderRadius: 20,
|
borderTopLeftRadius: isImageOnLeft ? 0 : 20,
|
||||||
|
borderTopRightRadius: isImageOnLeft ? 20 : 0,
|
||||||
|
borderBottomLeftRadius: isImageOnLeft ? 0 : 20,
|
||||||
|
borderBottomRightRadius: isImageOnLeft ? 20 : 0,
|
||||||
paddingHorizontal: 18,
|
paddingHorizontal: 18,
|
||||||
paddingVertical: 16,
|
paddingVertical: 16,
|
||||||
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{isLock && (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
backgroundColor: "rgba(0,0,0,0.6)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
minHeight: 100,
|
minHeight: 100,
|
||||||
|
|||||||
+109
-20
@@ -1,15 +1,22 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
import React, {
|
||||||
import { Platform, StyleSheet, View } from "react-native";
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { Alert, Platform, StyleSheet, View } from "react-native";
|
||||||
import { background } from "../../assets";
|
import { background } from "../../assets";
|
||||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
import BorderGradientButton from "../../components/BorderGradientButton";
|
||||||
import FeatureCarousel from "../../components/FeatureCarousel/FeatureCarousel";
|
import FeatureCarousel from "../../components/FeatureCarousel/FeatureCarousel";
|
||||||
import GradientButton from "../../components/GradientButton";
|
import GradientButton from "../../components/GradientButton";
|
||||||
|
import MoreMenu from "../../components/MoreMenu";
|
||||||
import ProjectDropDown from "../../components/ProjectDropDown/ProjectDropDown";
|
import ProjectDropDown from "../../components/ProjectDropDown/ProjectDropDown";
|
||||||
import ShareBtn from "../../components/ShareBtn/ShareBtn";
|
import ShareBtn from "../../components/ShareBtn/ShareBtn";
|
||||||
import { isWeb } from "../../hooks/useLayoutType.js";
|
import { isWeb } from "../../hooks/useLayoutType.js";
|
||||||
import Page from "../../layouts/Page";
|
import Page from "../../layouts/Page";
|
||||||
import { Routes } from "../../navigation";
|
|
||||||
import { navigate } from "../../navigation/NavigationService";
|
import { navigate } from "../../navigation/NavigationService";
|
||||||
|
import { projectsRef } from "../../config/firebase";
|
||||||
import { useUser } from "../../providers/UserDataProvider";
|
import { useUser } from "../../providers/UserDataProvider";
|
||||||
import { gutters, Palette } from "../../styles";
|
import { gutters, Palette } from "../../styles";
|
||||||
import {
|
import {
|
||||||
@@ -17,6 +24,7 @@ import {
|
|||||||
getCreationStageStates,
|
getCreationStageStates,
|
||||||
getStageAction,
|
getStageAction,
|
||||||
} from "../../utils/projectStages";
|
} from "../../utils/projectStages";
|
||||||
|
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const {
|
const {
|
||||||
@@ -26,10 +34,11 @@ const Home = () => {
|
|||||||
selectedProject,
|
selectedProject,
|
||||||
selectedProjectId,
|
selectedProjectId,
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
const { setTooltip } = useMinuit();
|
||||||
|
|
||||||
const projects = useMemo(
|
const projects = useMemo(
|
||||||
() => (Array.isArray(userProjects) ? userProjects : []),
|
() => (Array.isArray(userProjects) ? userProjects : []),
|
||||||
[userProjects]
|
[userProjects],
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentProject = useMemo(() => {
|
const currentProject = useMemo(() => {
|
||||||
@@ -60,22 +69,9 @@ const Home = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSelectProject = (project) => {
|
|
||||||
if (!project?.id) return;
|
|
||||||
selectProject(project.id);
|
|
||||||
setActiveStageIndex(findFirstUnlockedStageIndex(project));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleModifyProject = (project) => {
|
|
||||||
if (!project?.id) return;
|
|
||||||
selectProject(project.id);
|
|
||||||
setActiveStageIndex(findFirstUnlockedStageIndex(project));
|
|
||||||
navigate(Routes.Home);
|
|
||||||
};
|
|
||||||
|
|
||||||
const stageStates = useMemo(
|
const stageStates = useMemo(
|
||||||
() => getCreationStageStates(currentProject),
|
() => getCreationStageStates(currentProject),
|
||||||
[currentProject]
|
[currentProject],
|
||||||
);
|
);
|
||||||
|
|
||||||
const firstUnlockedIndex = useMemo(() => {
|
const firstUnlockedIndex = useMemo(() => {
|
||||||
@@ -84,6 +80,10 @@ const Home = () => {
|
|||||||
}, [stageStates]);
|
}, [stageStates]);
|
||||||
|
|
||||||
const [activeStageIndex, setActiveStageIndex] = useState(firstUnlockedIndex);
|
const [activeStageIndex, setActiveStageIndex] = useState(firstUnlockedIndex);
|
||||||
|
const [menuVisible, setMenuVisible] = useState(false);
|
||||||
|
const [menuAnchor, setMenuAnchor] = useState(null);
|
||||||
|
const [menuProject, setMenuProject] = useState(null);
|
||||||
|
const menuAnchorRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveStageIndex((prev) => {
|
setActiveStageIndex((prev) => {
|
||||||
@@ -99,6 +99,86 @@ const Home = () => {
|
|||||||
});
|
});
|
||||||
}, [stageStates, firstUnlockedIndex]);
|
}, [stageStates, firstUnlockedIndex]);
|
||||||
|
|
||||||
|
const handleSelectProject = useCallback(
|
||||||
|
(project) => {
|
||||||
|
if (!project?.id) return;
|
||||||
|
selectProject(project.id);
|
||||||
|
setActiveStageIndex(findFirstUnlockedStageIndex(project));
|
||||||
|
},
|
||||||
|
[selectProject],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCloseMenu = useCallback(() => {
|
||||||
|
setMenuVisible(false);
|
||||||
|
setMenuAnchor(null);
|
||||||
|
setMenuProject(null);
|
||||||
|
menuAnchorRef.current = null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDeleteProject = useCallback(async () => {
|
||||||
|
if (!menuProject?.id) return;
|
||||||
|
try {
|
||||||
|
await projectsRef.doc(menuProject.id).delete();
|
||||||
|
setTooltip?.({ type: "success", text: "Projet supprimé" });
|
||||||
|
} catch (error) {
|
||||||
|
setTooltip?.({
|
||||||
|
type: "error",
|
||||||
|
text: error?.message || "Suppression impossible",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
handleCloseMenu();
|
||||||
|
}
|
||||||
|
}, [handleCloseMenu, menuProject?.id, setTooltip]);
|
||||||
|
|
||||||
|
const handleModifyProject = useCallback((project, anchor) => {
|
||||||
|
if (!project?.id) return;
|
||||||
|
|
||||||
|
const prevAnchor = menuAnchorRef.current;
|
||||||
|
const normalizedAnchor =
|
||||||
|
typeof anchor === "number"
|
||||||
|
? { top: anchor }
|
||||||
|
: anchor && typeof anchor === "object"
|
||||||
|
? anchor
|
||||||
|
: {};
|
||||||
|
|
||||||
|
setMenuProject(project);
|
||||||
|
setMenuAnchor(normalizedAnchor);
|
||||||
|
setMenuVisible((prev) => {
|
||||||
|
if (!prev) return true;
|
||||||
|
const prevTop =
|
||||||
|
typeof prevAnchor?.top === "number" ? prevAnchor.top : null;
|
||||||
|
const nextTop =
|
||||||
|
typeof normalizedAnchor?.top === "number" ? normalizedAnchor.top : null;
|
||||||
|
return prevTop !== nextTop;
|
||||||
|
});
|
||||||
|
menuAnchorRef.current = normalizedAnchor;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const moreMenuItems = useMemo(() => {
|
||||||
|
if (!menuProject?.id) return [];
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: "Supprimer",
|
||||||
|
onPress: () =>
|
||||||
|
Alert.alert(
|
||||||
|
"Confirmer la suppression",
|
||||||
|
"Cette action supprimera définitivement ce projet.",
|
||||||
|
[
|
||||||
|
{ text: "Annuler", style: "cancel" },
|
||||||
|
{
|
||||||
|
text: "Supprimer",
|
||||||
|
style: "destructive",
|
||||||
|
onPress: () => {
|
||||||
|
handleDeleteProject();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{ cancelable: true },
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [handleDeleteProject, menuProject?.id]);
|
||||||
|
|
||||||
const activeStage =
|
const activeStage =
|
||||||
stageStates[activeStageIndex] || stageStates[firstUnlockedIndex];
|
stageStates[activeStageIndex] || stageStates[firstUnlockedIndex];
|
||||||
const stageAction = useMemo(() => {
|
const stageAction = useMemo(() => {
|
||||||
@@ -139,7 +219,7 @@ const Home = () => {
|
|||||||
|
|
||||||
const songwriterAction = useMemo(
|
const songwriterAction = useMemo(
|
||||||
() => getStageAction("songwriter", null),
|
() => getStageAction("songwriter", null),
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleStartNew = useCallback(() => {
|
const handleStartNew = useCallback(() => {
|
||||||
@@ -149,7 +229,7 @@ const Home = () => {
|
|||||||
console.log(
|
console.log(
|
||||||
"navigating to",
|
"navigating to",
|
||||||
songwriterAction.route,
|
songwriterAction.route,
|
||||||
songwriterAction.params
|
songwriterAction.params,
|
||||||
);
|
);
|
||||||
navigate(songwriterAction.route, songwriterAction.params);
|
navigate(songwriterAction.route, songwriterAction.params);
|
||||||
}
|
}
|
||||||
@@ -185,6 +265,15 @@ const Home = () => {
|
|||||||
/>
|
/>
|
||||||
{!isWeb && <ShareBtn />}
|
{!isWeb && <ShareBtn />}
|
||||||
</View>
|
</View>
|
||||||
|
<MoreMenu
|
||||||
|
visible={menuVisible}
|
||||||
|
top={menuAnchor?.top ?? 0}
|
||||||
|
position={menuAnchor}
|
||||||
|
onClose={handleCloseMenu}
|
||||||
|
inPlaylist={false}
|
||||||
|
projectId={menuProject?.id || null}
|
||||||
|
extraItems={moreMenuItems}
|
||||||
|
/>
|
||||||
<View style={styles.carouselSection}>
|
<View style={styles.carouselSection}>
|
||||||
<FeatureCarousel
|
<FeatureCarousel
|
||||||
selectedProject={currentProject}
|
selectedProject={currentProject}
|
||||||
|
|||||||
Reference in New Issue
Block a user