Merge branch 'main' of gitlab.com:agenceminuit/musicland

This commit is contained in:
2025-10-06 10:31:12 +02:00
7 changed files with 543 additions and 162 deletions
@@ -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
View File
@@ -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>
+140 -55
View File
@@ -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
View File
@@ -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}