Merge branch 'main' of gitlab.com:agenceminuit/musicland
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,
|
||||
} 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)",
|
||||
},
|
||||
});
|
||||
|
||||
+44
-68
@@ -9,6 +9,24 @@ import { Palette } from "../styles";
|
||||
import { FONT_FAMILY } from "../styles/Fonts";
|
||||
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 = ({
|
||||
visible = false,
|
||||
top = 0,
|
||||
@@ -26,8 +44,8 @@ const MoreMenu = ({
|
||||
typeof position?.top === "number"
|
||||
? position.top
|
||||
: typeof top === "number"
|
||||
? top
|
||||
: 0;
|
||||
? top
|
||||
: 0;
|
||||
const resolvedRight =
|
||||
typeof position?.right === "number" ? Math.max(position.right, 0) : null;
|
||||
const resolvedLeft =
|
||||
@@ -108,47 +126,7 @@ const MoreMenu = ({
|
||||
>
|
||||
<View style={{ gap: 2 }}>
|
||||
{inPlaylist && (
|
||||
<Pressable 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"
|
||||
// }
|
||||
>
|
||||
<MoreBtn onPress={removeFromPlaylist}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
@@ -156,14 +134,26 @@ const MoreMenu = ({
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
Ajouter à une playlist
|
||||
Supprimer de la playlist
|
||||
</Text>
|
||||
</BlurView>
|
||||
</Pressable>
|
||||
</MoreBtn>
|
||||
)}
|
||||
|
||||
<MoreBtn onPress={addToPlaylist}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
Ajouter à une playlist
|
||||
</Text>
|
||||
</MoreBtn>
|
||||
|
||||
{Array.isArray(extraItems) &&
|
||||
extraItems.map((item, idx) => (
|
||||
<Pressable
|
||||
<MoreBtn
|
||||
key={idx}
|
||||
onPress={() => {
|
||||
try {
|
||||
@@ -173,30 +163,16 @@ const MoreMenu = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<BlurView
|
||||
intensity={Platform.OS !== "ios" ? 10 : 20}
|
||||
<Text
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
backgroundColor: Palette.glass,
|
||||
borderRadius: 12,
|
||||
overflow: "hidden",
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
// experimentalBlurMethod={
|
||||
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
||||
// }
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
{item?.label || "Action"}
|
||||
</Text>
|
||||
</BlurView>
|
||||
</Pressable>
|
||||
{item?.label || "Action"}
|
||||
</Text>
|
||||
</MoreBtn>
|
||||
))}
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
Animated,
|
||||
Dimensions,
|
||||
Easing,
|
||||
FlatList,
|
||||
Image,
|
||||
Pressable,
|
||||
@@ -43,6 +46,7 @@ const ProjectDropDown = ({
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [triggerLayout, setTriggerLayout] = useState(null);
|
||||
const dropdownAnimation = useRef(new Animated.Value(0)).current;
|
||||
|
||||
const normalizedProjects = Array.isArray(projects) ? projects : [];
|
||||
const isDisabled = normalizedProjects.length === 0;
|
||||
@@ -85,9 +89,9 @@ const ProjectDropDown = ({
|
||||
);
|
||||
|
||||
const handleModify = useCallback(
|
||||
(project, topPosition) => {
|
||||
(project, anchor) => {
|
||||
if (!project?.id) return;
|
||||
onModifyProject?.(project, topPosition);
|
||||
onModifyProject?.(project, anchor);
|
||||
},
|
||||
[onModifyProject],
|
||||
);
|
||||
@@ -102,6 +106,52 @@ const ProjectDropDown = ({
|
||||
[triggerLayout],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
Animated.timing(dropdownAnimation, {
|
||||
toValue: isOpen ? 1 : 0,
|
||||
duration: 220,
|
||||
easing: isOpen ? Easing.out(Easing.cubic) : Easing.in(Easing.cubic),
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}, [dropdownAnimation, isOpen]);
|
||||
|
||||
const overlayTranslateY = useMemo(
|
||||
() =>
|
||||
dropdownAnimation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [-12, 0],
|
||||
}),
|
||||
[dropdownAnimation],
|
||||
);
|
||||
|
||||
const overlayScale = useMemo(
|
||||
() =>
|
||||
dropdownAnimation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0.96, 1],
|
||||
}),
|
||||
[dropdownAnimation],
|
||||
);
|
||||
|
||||
const triggerOpacity = useMemo(
|
||||
() =>
|
||||
dropdownAnimation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [1, 0],
|
||||
extrapolate: "clamp",
|
||||
}),
|
||||
[dropdownAnimation],
|
||||
);
|
||||
|
||||
const triggerTranslateY = useMemo(
|
||||
() =>
|
||||
dropdownAnimation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, -6],
|
||||
}),
|
||||
[dropdownAnimation],
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
{isOpen ? (
|
||||
@@ -112,44 +162,63 @@ const ProjectDropDown = ({
|
||||
style={styles.triggerWrapper}
|
||||
onLayout={(event) => setTriggerLayout(event.nativeEvent.layout)}
|
||||
>
|
||||
<BlurView
|
||||
intensity={BUTTON_BLUR_INTENSITY}
|
||||
tint="dark"
|
||||
<Animated.View
|
||||
pointerEvents={isOpen ? "none" : "auto"}
|
||||
style={[
|
||||
styles.dropdownBlur,
|
||||
isDisabled && styles.dropdownBlurDisabled,
|
||||
isOpen && styles.dropdownBlurHidden,
|
||||
styles.dropdownTriggerContainer,
|
||||
{
|
||||
opacity: triggerOpacity,
|
||||
transform: [{ translateY: triggerTranslateY }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ProjectRow
|
||||
isTitle={true}
|
||||
isOpen={isOpen}
|
||||
project={currentProject}
|
||||
formatDate={formatDate}
|
||||
onSelect={toggleDropdown}
|
||||
/>
|
||||
</BlurView>
|
||||
<BlurView
|
||||
intensity={BUTTON_BLUR_INTENSITY}
|
||||
tint="dark"
|
||||
style={[
|
||||
styles.dropdownBlur,
|
||||
isDisabled && styles.dropdownBlurDisabled,
|
||||
]}
|
||||
>
|
||||
<ProjectRow
|
||||
isTitle={true}
|
||||
isOpen={isOpen}
|
||||
project={currentProject}
|
||||
formatDate={formatDate}
|
||||
onSelect={toggleDropdown}
|
||||
/>
|
||||
</BlurView>
|
||||
</Animated.View>
|
||||
|
||||
<BlurView
|
||||
intensity={BUTTON_BLUR_INTENSITY}
|
||||
tint="dark"
|
||||
<Animated.View
|
||||
pointerEvents={isOpen ? "auto" : "none"}
|
||||
style={[
|
||||
styles.dropdownOverlay,
|
||||
styles.dropdownOverlayContainer,
|
||||
{ width: dropdownWidth || "100%" },
|
||||
!isOpen && styles.dropdownOverlayHidden,
|
||||
{
|
||||
opacity: dropdownAnimation,
|
||||
transform: [
|
||||
{ translateY: overlayTranslateY },
|
||||
{ scale: overlayScale },
|
||||
],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ProjectRow
|
||||
isTitle={true}
|
||||
isOpen={isOpen}
|
||||
project={currentProject}
|
||||
formatDate={formatDate}
|
||||
onSelect={toggleDropdown}
|
||||
/>
|
||||
<BlurView
|
||||
intensity={BUTTON_BLUR_INTENSITY}
|
||||
tint="dark"
|
||||
style={styles.dropdownOverlay}
|
||||
>
|
||||
<ProjectRow
|
||||
isTitle={true}
|
||||
isOpen={isOpen}
|
||||
project={currentProject}
|
||||
formatDate={formatDate}
|
||||
onSelect={toggleDropdown}
|
||||
/>
|
||||
|
||||
<View style={styles.dropdownOverlayContent}>
|
||||
<FlatList
|
||||
<View style={styles.dropdownOverlayContent}>
|
||||
<FlatList
|
||||
data={dropdownProjects}
|
||||
keyExtractor={(project, index) =>
|
||||
project?.id || project?.title || `project-${index}`
|
||||
@@ -168,13 +237,14 @@ const ProjectDropDown = ({
|
||||
contentContainerStyle={styles.dropdownListContent}
|
||||
style={styles.dropdownList}
|
||||
/>
|
||||
<GradientButton
|
||||
containerStyle={styles.createButtonContainer}
|
||||
title="Commencer à créer"
|
||||
onPress={handleCreate}
|
||||
/>
|
||||
</View>
|
||||
</BlurView>
|
||||
<GradientButton
|
||||
containerStyle={styles.createButtonContainer}
|
||||
title="Commencer à créer"
|
||||
onPress={handleCreate}
|
||||
/>
|
||||
</View>
|
||||
</BlurView>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -192,23 +262,37 @@ const ProjectRow = ({
|
||||
}) => {
|
||||
const rowRef = useRef(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(() => {
|
||||
if (!layout) return;
|
||||
const top = (layout?.y || 0) + 45;
|
||||
setMenuPos(top);
|
||||
const top = (layout?.y || 0) + (layout?.height || 45);
|
||||
setMenuPos((prev) => ({ ...prev, top }));
|
||||
}, [layout]);
|
||||
|
||||
const onPressMenu = () => {
|
||||
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) {
|
||||
try {
|
||||
rowRef.current.measureInWindow((x, y) => {
|
||||
const top = (y || 0) + 45;
|
||||
setMenuPos(top);
|
||||
onModify?.(project, top);
|
||||
rowRef.current.measureInWindow((x, y, width, height) => {
|
||||
updateAnchor(x, y, width, height);
|
||||
});
|
||||
return;
|
||||
} catch (_error) {
|
||||
@@ -295,29 +379,30 @@ const styles = StyleSheet.create({
|
||||
dropdownBlurDisabled: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
dropdownBlurHidden: {
|
||||
opacity: 0,
|
||||
pointerEvents: "none",
|
||||
},
|
||||
triggerWrapper: {
|
||||
position: "relative",
|
||||
zIndex: 3,
|
||||
},
|
||||
dropdownOverlay: {
|
||||
dropdownTriggerContainer: {
|
||||
width: "100%",
|
||||
},
|
||||
dropdownOverlayContainer: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
borderRadius: 16,
|
||||
overflow: "hidden",
|
||||
zIndex: 4,
|
||||
elevation: 4,
|
||||
maxHeight: 400,
|
||||
},
|
||||
dropdownOverlay: {
|
||||
flex: 1,
|
||||
width: "100%",
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 8,
|
||||
gap: 9,
|
||||
maxHeight: 400,
|
||||
zIndex: 4,
|
||||
elevation: 4,
|
||||
overflow: "hidden",
|
||||
},
|
||||
dropdownOverlayHidden: {
|
||||
opacity: 0,
|
||||
},
|
||||
dropdownOverlayContent: {
|
||||
maxHeight: 260,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { BlurView } from "expo-blur";
|
||||
import { gutters, Palette } from "../../../styles";
|
||||
@@ -7,10 +7,10 @@ import { FONT_FAMILY } from "../../../styles/Fonts";
|
||||
import FontAwesome from "@expo/vector-icons/FontAwesome";
|
||||
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 baseHeight = Math.max(height ?? windowHeight, 1);
|
||||
const cardHeight = Platform.OS === "web" ? Math.round(baseHeight) : baseHeight;
|
||||
const cardHeight = Math.max(height ?? windowHeight, 1);
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -38,6 +38,8 @@ export default function PersonaCard({ item, isLock = false, height }) {
|
||||
tint="dark"
|
||||
style={{
|
||||
borderRadius: 20,
|
||||
borderTopLeftRadius: isImageOnLeft ? 20 : 0,
|
||||
borderTopRightRadius: isImageOnLeft ? 0 : 20,
|
||||
paddingVertical: 20,
|
||||
paddingHorizontal: gutters,
|
||||
overflow: "hidden",
|
||||
@@ -74,6 +76,8 @@ export default function PersonaCard({ item, isLock = false, height }) {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
borderRadius: 20,
|
||||
borderTopLeftRadius: isImageOnLeft ? 20 : 0,
|
||||
borderTopRightRadius: isImageOnLeft ? 0 : 20,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: "rgba(0,0,0,0.2)",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { BlurView } from "expo-blur";
|
||||
import { Palette } from "../../../styles";
|
||||
@@ -44,11 +44,23 @@ export default function PersonaCard({ item, index, isLock = true }) {
|
||||
tint="dark"
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: 20,
|
||||
borderTopLeftRadius: isImageOnLeft ? 0 : 20,
|
||||
borderTopRightRadius: isImageOnLeft ? 20 : 0,
|
||||
borderBottomLeftRadius: isImageOnLeft ? 0 : 20,
|
||||
borderBottomRightRadius: isImageOnLeft ? 20 : 0,
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 16,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{isLock && (
|
||||
<View
|
||||
style={{
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: "rgba(0,0,0,0.6)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
minHeight: 100,
|
||||
|
||||
+109
-20
@@ -1,15 +1,22 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Platform, StyleSheet, View } from "react-native";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Alert, Platform, StyleSheet, View } from "react-native";
|
||||
import { background } from "../../assets";
|
||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
import FeatureCarousel from "../../components/FeatureCarousel/FeatureCarousel";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import MoreMenu from "../../components/MoreMenu";
|
||||
import ProjectDropDown from "../../components/ProjectDropDown/ProjectDropDown";
|
||||
import ShareBtn from "../../components/ShareBtn/ShareBtn";
|
||||
import { isWeb } from "../../hooks/useLayoutType.js";
|
||||
import Page from "../../layouts/Page";
|
||||
import { Routes } from "../../navigation";
|
||||
import { navigate } from "../../navigation/NavigationService";
|
||||
import { projectsRef } from "../../config/firebase";
|
||||
import { useUser } from "../../providers/UserDataProvider";
|
||||
import { gutters, Palette } from "../../styles";
|
||||
import {
|
||||
@@ -17,6 +24,7 @@ import {
|
||||
getCreationStageStates,
|
||||
getStageAction,
|
||||
} from "../../utils/projectStages";
|
||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||
|
||||
const Home = () => {
|
||||
const {
|
||||
@@ -26,10 +34,11 @@ const Home = () => {
|
||||
selectedProject,
|
||||
selectedProjectId,
|
||||
} = useUser();
|
||||
const { setTooltip } = useMinuit();
|
||||
|
||||
const projects = useMemo(
|
||||
() => (Array.isArray(userProjects) ? userProjects : []),
|
||||
[userProjects]
|
||||
[userProjects],
|
||||
);
|
||||
|
||||
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(
|
||||
() => getCreationStageStates(currentProject),
|
||||
[currentProject]
|
||||
[currentProject],
|
||||
);
|
||||
|
||||
const firstUnlockedIndex = useMemo(() => {
|
||||
@@ -84,6 +80,10 @@ const Home = () => {
|
||||
}, [stageStates]);
|
||||
|
||||
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(() => {
|
||||
setActiveStageIndex((prev) => {
|
||||
@@ -99,6 +99,86 @@ const Home = () => {
|
||||
});
|
||||
}, [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 =
|
||||
stageStates[activeStageIndex] || stageStates[firstUnlockedIndex];
|
||||
const stageAction = useMemo(() => {
|
||||
@@ -139,7 +219,7 @@ const Home = () => {
|
||||
|
||||
const songwriterAction = useMemo(
|
||||
() => getStageAction("songwriter", null),
|
||||
[]
|
||||
[],
|
||||
);
|
||||
|
||||
const handleStartNew = useCallback(() => {
|
||||
@@ -149,7 +229,7 @@ const Home = () => {
|
||||
console.log(
|
||||
"navigating to",
|
||||
songwriterAction.route,
|
||||
songwriterAction.params
|
||||
songwriterAction.params,
|
||||
);
|
||||
navigate(songwriterAction.route, songwriterAction.params);
|
||||
}
|
||||
@@ -185,6 +265,15 @@ const Home = () => {
|
||||
/>
|
||||
{!isWeb && <ShareBtn />}
|
||||
</View>
|
||||
<MoreMenu
|
||||
visible={menuVisible}
|
||||
top={menuAnchor?.top ?? 0}
|
||||
position={menuAnchor}
|
||||
onClose={handleCloseMenu}
|
||||
inPlaylist={false}
|
||||
projectId={menuProject?.id || null}
|
||||
extraItems={moreMenuItems}
|
||||
/>
|
||||
<View style={styles.carouselSection}>
|
||||
<FeatureCarousel
|
||||
selectedProject={currentProject}
|
||||
|
||||
Reference in New Issue
Block a user