home, subscription and other fixes

This commit is contained in:
Thomas Demirdjian
2025-11-06 17:33:07 +01:00
parent 5f777f3b6d
commit 4365f1e46d
31 changed files with 1579 additions and 348 deletions
+226 -226
View File
@@ -5,13 +5,11 @@ import React, {
useRef,
useState,
} from "react";
import { Platform, StyleSheet, View } from "react-native";
import { useIsFocused } from "@react-navigation/native";
import { StyleSheet, Text, View } from "react-native";
import { Image as ExpoImage } from "expo-image";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import { background, videos } from "../../assets";
import { background, cardsImg, icons, videos } from "../../assets";
import Alert from "../../components/Alert";
import BorderGradientButton from "../../components/BorderGradientButton";
import FeatureCarousel from "../../components/FeatureCarousel/FeatureCarousel";
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
import GradientButton from "../../components/GradientButton";
import MoreMenu from "../../components/MoreMenu";
@@ -26,11 +24,13 @@ import { navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation/Routes";
import { useUser } from "../../providers/UserDataProvider";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import {
findFirstUnlockedStageIndex,
getCreationStageStates,
getStageAction,
} from "../../utils/projectStages";
import StageCard from "./components/StageCard";
import ClubCard from "./components/ClubCard";
const isProjectEmpty = (project) => {
if (!project) {
@@ -42,8 +42,55 @@ const isProjectEmpty = (project) => {
);
};
const HOME_BACKGROUND_WIDTH = isWeb ? 1280 : 520;
const HOME_BACKGROUND_HEIGHT = isWeb ? 760 : 360;
const STAGE_CARD_CONTENT = [
{
key: "songwriter",
step: "ÉTAPE 1",
description:
"Ensemble, nous allons écrire ta chanson.\nMéthodiquement je vais te guider pour construire une oeuvre unique et authentique.",
image: cardsImg.writing,
imagePosition: "left",
textAlign: "left",
lockSide: "right",
},
{
key: "beatmaker",
step: "ÉTAPE 2",
description:
"Je suis Malik, responsable du studio de musicland, je vais mettre en musique tes paroles en fonction de tes goûts musicaux, ça va être top!",
image: cardsImg.studio,
imagePosition: "right",
textAlign: "right",
lockSide: "left",
},
{
key: "director",
step: "ÉTAPE 3",
description:
"Tu vas faire une expérience extraordinaire, tu vas te filmer en train dinterpreter ta chanson en Play Back et je vais te guider pour te faciliter la tâche!",
image: cardsImg.video,
imagePosition: "left",
textAlign: "left",
lockSide: "right",
},
{
key: "publisher",
step: "ÉTAPE 4",
description:
"Je suis Mr Benhaï, Producteur de MusicLand et je vais te faire une proposition qui pourrait tintéresser, on se retrouve à la sotie du studio!",
image: cardsImg.production,
imagePosition: "right",
textAlign: "right",
lockSide: "left",
},
];
const CLUB_CARD_IMAGE = icons.clubIcon;
const Home = ({ navigation, route }) => {
const isFocused = useIsFocused();
const {
userProjects = [],
selectProject,
@@ -61,7 +108,7 @@ const Home = ({ navigation, route }) => {
const projects = useMemo(
() => (Array.isArray(userProjects) ? userProjects : []),
[userProjects]
[userProjects],
);
const currentProject = useMemo(() => {
@@ -94,73 +141,34 @@ const Home = ({ navigation, route }) => {
const stageStates = useMemo(
() => getCreationStageStates(currentProject),
[currentProject]
[currentProject],
);
const preferredStageIndex = useMemo(() => {
if (!stageStates.length) {
return 0;
const stageStatesByKey = useMemo(() => {
if (!Array.isArray(stageStates)) {
return {};
}
const actionableIndex = stageStates.findIndex(
(stage) => !stage.isCompleted && !stage.isLocked
);
if (actionableIndex !== -1) {
return actionableIndex;
}
const firstIncomplete = stageStates.findIndex(
(stage) => !stage.isCompleted
);
if (firstIncomplete !== -1) {
return firstIncomplete;
}
const firstUnlocked = stageStates.findIndex((stage) => !stage.isLocked);
if (firstUnlocked !== -1) {
return firstUnlocked;
}
return stageStates.length - 1;
return stageStates.reduce((acc, stage) => {
if (stage?.key) {
acc[stage.key] = stage;
}
return acc;
}, {});
}, [stageStates]);
const [activeStageIndex, setActiveStageIndex] = useState(preferredStageIndex);
const [menuVisible, setMenuVisible] = useState(false);
const [menuAnchor, setMenuAnchor] = useState(null);
const [menuProject, setMenuProject] = useState(null);
const menuAnchorRef = useRef(null);
const testLoaderTimeoutRef = useRef(null);
const [isIntroVideoVisible, setIsIntroVideoVisible] = useState(false);
const [hasLocalAdventureFlag, setHasLocalAdventureFlag] = useState(false);
useEffect(() => {
setActiveStageIndex((prev) => {
const fallbackIndex =
preferredStageIndex >= 0 && preferredStageIndex < stageStates.length
? preferredStageIndex
: 0;
if (prev == null || prev >= stageStates.length) {
return fallbackIndex;
}
const current = stageStates[prev];
if (!current) {
return fallbackIndex;
}
if (
(current.isLocked || current.isCompleted) &&
fallbackIndex !== prev &&
stageStates[fallbackIndex]
) {
return fallbackIndex;
}
return prev;
});
}, [stageStates, preferredStageIndex]);
const handleSelectProject = useCallback(
(project) => {
if (!project?.id) return;
selectProject(project.id);
setActiveStageIndex(findFirstUnlockedStageIndex(project));
},
[selectProject]
[selectProject],
);
const handleCloseMenu = useCallback(() => {
@@ -228,7 +236,7 @@ const Home = ({ navigation, route }) => {
},
},
],
{ cancelable: true }
{ cancelable: true },
),
},
];
@@ -262,7 +270,7 @@ const Home = ({ navigation, route }) => {
navigate(targetRoute, beatmakerStage?.params);
},
},
]
],
);
navigation?.setParams?.({ showLyricsCongrats: false });
}, [
@@ -273,19 +281,6 @@ const Home = ({ navigation, route }) => {
route?.params?.showLyricsCongrats,
]);
const activeStage =
stageStates[activeStageIndex] || stageStates[preferredStageIndex];
const stageAction = useMemo(() => {
if (!activeStage) {
return null;
}
return getStageAction(activeStage.key, currentProject);
}, [activeStage, currentProject]);
const stageRoute = stageAction?.route || null;
const stageParams = stageAction?.params;
const stageLocked = activeStage?.isLocked ?? true;
const hasActiveProject = !!currentProject;
const ensureProjectSelected = useCallback(() => {
@@ -297,32 +292,9 @@ const Home = ({ navigation, route }) => {
}
}, [currentProject?.id, selectProject, selectedProject?.id]);
const handleStageAction = useCallback(() => {
if (!hasActiveProject || stageLocked || !stageRoute) {
return;
}
ensureProjectSelected();
navigate(stageRoute, stageParams);
}, [
ensureProjectSelected,
hasActiveProject,
stageLocked,
stageParams,
stageRoute,
]);
useEffect(() => {
return () => {
if (testLoaderTimeoutRef.current) {
clearTimeout(testLoaderTimeoutRef.current);
testLoaderTimeoutRef.current = null;
}
};
}, []);
const songwriterAction = useMemo(
() => getStageAction("songwriter", null),
[]
[],
);
const handleStartNew = useCallback(async () => {
@@ -335,7 +307,6 @@ const Home = ({ navigation, route }) => {
if (emptyProject?.id) {
selectProject(emptyProject.id);
setActiveStageIndex(findFirstUnlockedStageIndex(emptyProject));
navigate(targetRoute, targetParams);
return;
}
@@ -344,7 +315,6 @@ const Home = ({ navigation, route }) => {
if (!newProjectId) {
return;
}
setActiveStageIndex(0);
navigate(targetRoute, targetParams);
} catch (error) {
console.warn("Home: unable to start new project", error);
@@ -353,17 +323,7 @@ const Home = ({ navigation, route }) => {
text: "Impossible de démarrer un nouveau projet",
});
}
}, [
createNewProject,
projects,
selectProject,
setActiveStageIndex,
setTooltip,
songwriterAction,
]);
const continueDisabled = !hasActiveProject || stageLocked || !stageRoute;
const startDisabled = !songwriterAction?.route;
}, [createNewProject, projects, selectProject, setTooltip, songwriterAction]);
useEffect(() => {
if (currentUserData?.adventureStarted) {
@@ -412,104 +372,102 @@ const Home = ({ navigation, route }) => {
simpleRef: true,
});
const videoUrl = isWeb ? video?.landingWeb : video?.landing;
const homeBackgroundImage = isWeb
? background.homeBGWeb
: background.homeBG;
const homeBackgroundImage = background.bgTrans;
const landingBackgroundImage = homeBackgroundImage;
const stageCards = useMemo(
() =>
STAGE_CARD_CONTENT.map((card) => ({
...card,
isLocked: stageStatesByKey[card.key]?.isLocked ?? true,
})),
[stageStatesByKey],
);
const handleStagePress = useCallback(
(stageKey, isLocked) => {
if (!hasActiveProject || isLocked) {
return;
}
ensureProjectSelected();
const action = getStageAction(stageKey, currentProject);
if (!action?.route) {
return;
}
navigate(action.route, action.params);
},
[currentProject, ensureProjectSelected, hasActiveProject],
);
const handleClubPress = useCallback(() => {
navigate(Routes.Payments);
}, []);
return adventureStarted ? (
<Page
shareBtn
headerType="NONE"
containerStyle={{ width: "100%", maxWidth: "100%", paddingHorizontal: 0 }}
backgroundImg={homeBackgroundImage}
>
<View style={[styles.root, isWeb ? styles.rootWeb : styles.rootNative]}>
<View
style={{
zIndex: 10,
position: "absolute",
top: 10,
width: 400,
alignSelf: "center",
flexDirection: isWeb ? "column" : "row",
alignItems: "center",
paddingHorizontal: isWeb ? 0 : 30,
gap: 10,
}}
>
<ProjectDropDown
style={[isWeb ? { width: "100%" } : { flex: 1 }]}
projects={projects}
selectedProject={currentProject}
allowEmptySelection
onSelectProject={handleSelectProject}
onModifyProject={handleModifyProject}
onCreateProject={handleStartNew}
formatDate={formatDate}
/>
{!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, styles.carouselSectionElevated]}>
<FeatureCarousel
selectedProject={currentProject}
stageStates={stageStates}
activeIndex={activeStageIndex}
onActiveIndexChange={setActiveStageIndex}
backgroundImage={homeBackgroundImage}
isFocused={isFocused}
/>
</View>
<View
style={{
position: "absolute",
bottom: isWeb ? 180 : 140,
flexDirection: isWeb ? "row" : "column",
gap: 5,
alignSelf: "center",
}}
>
<GradientButton
containerStyle={{ width: Platform.OS === "web" ? 250 : "100%" }}
title={"Commencer à créer"}
onPress={handleStartNew}
disabled={startDisabled}
/>
<BorderGradientButton
containerStyle={{ width: Platform.OS === "web" ? 250 : "100%" }}
title={"Continuer la création"}
onPress={handleStageAction}
disabled={continueDisabled}
<View style={styles.root}>
<Page
shareBtn
headerType="NONE"
scrollEnabled={false}
containerStyle={styles.page}
contentContainerStyle={styles.pageContent}
width="100%"
maxWidth={null}
backgroundColor="#303438"
>
<View style={styles.inner}>
<ExpoImage
source={homeBackgroundImage}
contentFit="cover"
style={styles.centerImage}
/>
{/* <BorderGradientButton
containerStyle={{ width: Platform.OS === "web" ? 250 : "100%" }}
title={"Test Alert"}
onPress={() =>
Alert("Test", "Ceci est un test d'alert", [
{ text: "Annuler", style: "cancel" },
{ text: "OK", onPress: () => console.log("OK pressé") },
])
}
/> */}
{/* <BorderGradientButton
containerStyle={{ width: Platform.OS === "web" ? 250 : "100%" }}
title={"Continuer la création"}
onPress={() => navigate(Routes.SongReady)}
disabled={continueDisabled}
/> */}
<View style={styles.topBar}>
<ProjectDropDown
style={styles.projectDropDown}
projects={projects}
selectedProject={currentProject}
allowEmptySelection
onSelectProject={handleSelectProject}
onModifyProject={handleModifyProject}
onCreateProject={handleStartNew}
formatDate={formatDate}
/>
{!isWeb && <ShareBtn />}
</View>
<MoreMenu
visible={menuVisible}
top={menuAnchor?.top ?? 0}
position={menuAnchor}
onClose={handleCloseMenu}
inPlaylist={false}
projectId={menuProject?.id || null}
extraItems={moreMenuItems}
/>
<Text style={styles.subtitle}>5 espaces à découvrir</Text>
<View style={styles.cardsGrid}>
{stageCards.map((card) => (
<StageCard
key={card.key}
step={card.step}
description={card.description}
image={card.image}
imagePosition={card.imagePosition}
textAlign={card.textAlign}
isLocked={card.isLocked}
lockSide={card.lockSide}
onPress={() => handleStagePress(card.key, card.isLocked)}
/>
))}
</View>
<ClubCard image={CLUB_CARD_IMAGE} onPress={handleClubPress} />
</View>
</View>
</Page>
</Page>
</View>
) : (
<>
<Page
@@ -545,33 +503,75 @@ export default Home;
const styles = StyleSheet.create({
root: {
flex: 1,
backgroundColor: "#303438",
},
rootWeb: {
page: {
backgroundColor: "transparent",
},
rootNative: {
backgroundColor: "rgba(66, 91, 135, 0.3)",
},
heroImage: {
position: "absolute",
top: -60,
alignSelf: "center",
width: "80%",
maxWidth: 920,
height: 420,
opacity: 0.9,
},
shareIcon: {
width: 18,
height: 18,
tintColor: Palette.white,
},
carouselSection: {
padding: 0,
paddingLeft: isWeb ? 32 : 0,
paddingRight: isWeb ? 32 : 0,
paddingTop: 0,
paddingBottom: 0,
width: "100%",
flex: 1,
alignSelf: "stretch",
},
carouselSectionElevated: {
marginTop: -100,
paddingTop: 80,
pageContent: {
flexGrow: 1,
},
inner: {
flex: 1,
width: "100%",
alignSelf: "stretch",
alignItems: "stretch",
justifyContent: "flex-start",
position: "relative",
},
centerImage: {
width: HOME_BACKGROUND_WIDTH + 120,
height: HOME_BACKGROUND_HEIGHT + 80,
borderRadius: 22,
overflow: "hidden",
position: "absolute",
top: "45%",
left: "50%",
transform: [
{ translateX: -HOME_BACKGROUND_WIDTH / 2 },
{ translateY: -HOME_BACKGROUND_HEIGHT / 2 },
],
pointerEvents: "none",
},
topBar: {
width: "100%",
flexDirection: "row",
flexWrap: "wrap",
alignItems: "center",
justifyContent: "center",
gap: 12,
marginTop: 24,
marginBottom: 8,
zIndex: 10,
},
projectDropDown: {
width: isWeb ? 420 : "100%",
maxWidth: 420,
flexGrow: 1,
},
subtitle: {
width: "100%",
fontFamily: FONT_FAMILY.InterSemiBold,
fontSize: 18,
color: Palette.white,
textAlign: "center",
textTransform: "uppercase",
letterSpacing: 1,
marginBottom: 16,
},
cardsGrid: {
width: "100%",
flexDirection: "row",
flexWrap: "wrap",
justifyContent: "space-between",
rowGap: 20,
columnGap: 20,
},
});