Change main page and modify flows

This commit is contained in:
Thomas Demirdjian
2025-09-02 11:45:27 +02:00
parent 8b7980308a
commit 98367af0b7
47 changed files with 3695 additions and 1020 deletions
+61 -44
View File
@@ -1,13 +1,13 @@
import { View, Dimensions } from "react-native";
import React, { useMemo, useRef, useState } from "react";
import { useRoute } from "@react-navigation/native";
import Page from "../../layouts/Page";
import MusicLandHeader from "../../components/MusicLandHeader";
import GradientButton from "../../components/GradientButton";
import { gutters } from "../../styles";
import { SwiperFlatList } from "react-native-swiper-flatlist";
import Goals from "./Goals";
import { goBack } from "../../navigation/NavigationService";
import { goBack, navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation";
import SpecificityContext from "./SpecificityContext";
import EmotionConvey from "./EmotionConvey";
import SongStyle from "./SongStyle";
@@ -20,42 +20,24 @@ import { responsiveHeight } from "react-native-responsive-dimensions";
const { width } = Dimensions.get("window");
const CreateLyricsWithAi = () => {
const CreateLyricsWithAi = ({ route }) => {
const { hasLyrics = false, regenerateKey } = route?.params || {};
const scrollRef = useRef(null);
const route = useRoute();
const regenerateKey = route?.params?.regenerateKey;
const [selectedIndex, setSelectedIndex] = useState(0);
// If user already has lyrics, start at SongStructure (index 5)
const [selectedIndex, setSelectedIndex] = useState(hasLyrics ? 5 : 0);
const [progress, setProgress] = useState(16);
const [parentLayout, setparentLayout] = useState(null);
// Collected state across steps
const [objective, setObjective] = useState(
__DEV__ ? "Pour mon entreprise" : null,
); // from Goals list
const [objective, setObjective] = useState(null); // from Goals list
const [otherObjective, setOtherObjective] = useState("");
const [context, setContext] = useState(
__DEV__
? "L'agence minuit est une agence de développement mobile et web qui accompagnes ses client dans la réalisations de projets divers et variés"
: "",
);
const [emotion, setEmotion] = useState(
__DEV__
? {
title: "La Joie",
description:
"expose un bonheur profond, l'émerveillement, la gratitude, satisfaction intense, énergie positive",
}
: null,
); // { title, description }
const [style, setStyle] = useState(__DEV__ ? "Upbeat" : null); // from list
const [context, setContext] = useState("");
const [emotion, setEmotion] = useState(null);
const [style, setStyle] = useState(null); // from list
const [otherStyle, setOtherStyle] = useState("");
const [audience, setAudience] = useState(
__DEV__ ? "Aux clients de l'agence minuit" : "",
);
const [structure, setStructure] = useState(
__DEV__ ? "1 couplet, 1 refrain, 1 couplet, 1 refrain" : null,
); // selected structure string
const [rhymes, setRhymes] = useState(__DEV__ ? "Avec rimes" : null);
const [audience, setAudience] = useState("");
const [structure, setStructure] = useState(null); // selected structure string
const [rhymes, setRhymes] = useState(null);
const [customStructure, setCustomStructure] = useState(null); // array like ['couplet','refrain']
const parsedStructure = useMemo(() => {
@@ -117,27 +99,61 @@ const CreateLyricsWithAi = () => {
]);
const onPressNext = () => {
setSelectedIndex(selectedIndex + 1);
setProgress(progress + 9);
scrollRef.current.scrollToIndex({
index: selectedIndex + 1,
animated: true,
});
// If user already has lyrics and just finished CustomizeSongStructure (index 6),
// skip AI generation and go straight to Lyrics editor
if (hasLyrics && selectedIndex === 6) {
const chosenStructure =
(customStructure &&
parsedStructure &&
customStructure.length === parsedStructure.length
? customStructure
: parsedStructure) || [];
navigate(Routes.Lyrics, {
config: { structure: chosenStructure },
selections: {
objective,
otherObjective,
context,
emotion,
style,
otherStyle,
audience,
structure,
parsedStructure,
customStructure,
rhymes,
},
hasLyrics: true,
});
return;
}
setSelectedIndex((idx) => idx + 1);
};
const onPressBack = () => {
if (selectedIndex > 0) {
setSelectedIndex(selectedIndex - 1);
setProgress(progress - 9);
scrollRef.current.scrollToIndex({
index: selectedIndex - 1,
animated: true,
});
// If currently on SongStructure, go back to previous screen instead of previous step
if (selectedIndex === 5) {
goBack();
} else if (selectedIndex > 0) {
setSelectedIndex((idx) => Math.max(0, idx - 1));
} else {
goBack();
}
};
// Sync scroll position and progress with selectedIndex
React.useEffect(() => {
if (!parentLayout?.height) return;
try {
const nextProgress = 16 + selectedIndex * 9;
setProgress(nextProgress);
scrollRef.current?.scrollToIndex?.({
index: selectedIndex,
animated: true,
});
} catch (_) {}
}, [selectedIndex, parentLayout?.height]);
return (
<Page headerType="NONE">
<MusicLandHeader
@@ -160,6 +176,7 @@ const CreateLyricsWithAi = () => {
style={{ width, left: -gutters }}
ref={scrollRef}
disableGesture
index={selectedIndex}
>
<View
style={{
+1
View File
@@ -168,6 +168,7 @@ const CreatingLyrics = ({ active, config, regenerateKey, selections }) => {
lyricsData: result,
config,
selections,
hasLyrics: false,
})
}
/>
+85 -70
View File
@@ -1,84 +1,93 @@
import { View, Text, StyleSheet, ScrollView } from "react-native";
import React, { useMemo, useState } from "react";
import CreateLyricsHeader from "./components/CreateLyricsHeader";
import { View, StyleSheet, Image, Platform, Text } from "react-native";
import React, { useMemo, useState, useCallback, useEffect } from "react";
import { BlurView } from "expo-blur";
import { Palette } from "../../styles";
import CreateLyricsHeader from "./components/CreateLyricsHeader";
import { Palette, Style } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
import { CUSTOM_SONG_STRUCTURE } from "../../data/data";
import SongStructureDragDrop from "../../components/SongStructureDragDrop";
import Sortable from "react-native-sortables";
import { icons } from "../../assets";
// baseStructure: array like ['couplet','refrain',...]
// onChange: callback that receives array like ['couplet','refrain',...]
const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
const [containerLayout, setContainerLayout] = useState(null);
const sourceItems = useMemo(() => {
// create unique ids for duplicates
// Build stable keyed data like { 'couplet-1': { label, value }, ... }
const data = useMemo(() => {
const counters = { couplet: 0, refrain: 0 };
return (baseStructure || []).map((type) => {
const key = (type || '').toLowerCase();
const out = {};
(baseStructure || []).forEach((type) => {
const key = (type || "").toLowerCase();
counters[key] = (counters[key] || 0) + 1;
const idx = counters[key];
return {
id: `${key}-${idx}`,
label: `${type} ${idx}`,
const id = `${key}-${idx}`;
out[id] = {
id,
label: `${key === "couplet" ? "Couplet" : "Refrain"} ${idx}`,
value: key,
};
});
return out;
}, [baseStructure]);
const items = useMemo(() => Object.values(data), [data]);
// Initialize parent with current order (so it's saved even without drag)
useEffect(() => {
const initialValues = (items || []).map((it) => it?.value).filter(Boolean);
onChange?.(initialValues);
// We only want to run when baseStructure-derived items change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [items.length]);
const Row = ({ item: rowData }) => (
<View style={styles.itemContainer}>
<BlurView
intensity={30}
style={styles.rowBlur}
experimentalBlurMethod={Platform.OS !== "ios" ? "dimezisBlurView" : "none"}
>
<View style={{ ...Style.containerSpaceBetween, alignItems: "center" }}>
<View>
<View>
<View>
<View />
</View>
</View>
<View>
<View />
</View>
</View>
<View style={{ flex: 1, justifyContent: "center" }}>
<View>
<Text style={styles.rowText}>{rowData?.label || ""}</Text>
</View>
</View>
<Sortable.Handle>
<Image source={icons.dragDots} style={styles.handleIcon} />
</Sortable.Handle>
</View>
</BlurView>
</View>
);
return (
<View style={{ flex: 1, gap: 10, marginTop: 16 }}>
<CreateLyricsHeader
title="Personnalise la structure de ta chanson"
subTitle="Intègre en Drag and drop"
subTitle="Réorganise par glisser-déposer"
/>
{/* <View style={{ flex: 1, gap: 10 }}>
<View
style={{ flex: 1 }}
onLayout={(e) => {
setContainerLayout(e.nativeEvent.layout);
<View style={{ flex: 1 }} onLayout={(e) => setContainerLayout(e.nativeEvent.layout)}>
<Sortable.Grid
columns={1}
rowGap={12}
data={items}
renderItem={({ item }) => <Row item={item} />}
customHandle
onDragEnd={({ data: newData }) => {
const values = (newData || []).map((it) => it?.value).filter(Boolean);
onChange?.(values);
}}
>
<ItemContainer height={containerLayout?.height}>
<ScrollView contentContainerStyle={{ gap: 10, paddingBottom: 20 }}>
{["Couplet", "Refrain", "Couplet", "Refrain"].map(
(item, index) => (
<View key={index} style={styles.dropzoneContainer}>
<Text style={styles.dropzone}>{item}</Text>
</View>
)
)}
</ScrollView>
</ItemContainer>
</View>
<View style={{ flex: 1 }}>
<ItemContainer height={containerLayout?.height}>
<ScrollView contentContainerStyle={{ gap: 10, paddingBottom: 20 }}>
{CUSTOM_SONG_STRUCTURE.map((item, index) => (
<View key={index} style={styles.itemContainer}>
<BlurView
intensity={30}
style={{
flex: 1,
backgroundColor: Palette.glass,
justifyContent: "center",
paddingHorizontal: 12,
}}
>
<Text style={styles.dropzone}>{item}</Text>
</BlurView>
</View>
))}
</ScrollView>
</ItemContainer>
</View>
</View> */}
<View style={{ flex: 1 }}>
<SongStructureDragDrop
sourceItems={sourceItems}
onChange={(arr) => onChange?.(arr)}
/>
</View>
</View>
@@ -88,17 +97,6 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
export default CustomizeSongStructure;
const styles = StyleSheet.create({
dropzoneContainer: {
paddingVertical: 14,
paddingHorizontal: 12,
backgroundColor: "#00000080",
borderRadius: 14,
},
dropzone: {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
},
itemContainer: {
height: 54,
backgroundColor: Palette.glass,
@@ -113,4 +111,21 @@ const styles = StyleSheet.create({
shadowRadius: 3.84,
elevation: 5,
},
rowBlur: {
flex: 1,
backgroundColor: Palette.glass,
justifyContent: "center",
paddingHorizontal: 12,
height: "100%",
},
rowText: {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
},
handleIcon: {
width: 22,
height: 22,
tintColor: Palette.white,
},
});
+6 -4
View File
@@ -1,13 +1,15 @@
import { View, Text, FlatList } from "react-native";
import React, { useState } from "react";
import CreateLyricsHeader from "./components/CreateLyricsHeader";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { EMOTION_CONVEY } from "../../data/data";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
const EmotionConvey = ({ selected: selectedProp, setSelected: setSelectedProp }) => {
const EmotionConvey = ({
selected: selectedProp,
setSelected: setSelectedProp,
}) => {
const [containerLayout, setContainerLayout] = useState(null);
const [internalSelected, setInternalSelected] = useState(null);
const selected = selectedProp ?? internalSelected;
@@ -25,8 +27,8 @@ const EmotionConvey = ({ selected: selectedProp, setSelected: setSelectedProp })
return (
<View style={{ flex: 1, gap: 10, marginTop: 16 }}>
<CreateLyricsHeader
title={`Quel émotion veux-tu\ntransmettre ?`}
subTitle="Sélectionne tes intentions émotionnelles. (2 max)"
title={`Quelle émotion veux-tu\ntransmettre ?`}
subTitle="Sélectionne une seule intention émotionnelle."
/>
<View
+43 -14
View File
@@ -1,17 +1,16 @@
import { View, Text, ScrollView, Alert } from "react-native";
import { View, ScrollView, Alert } from "react-native";
import React, { useMemo, useState, useCallback } from "react";
import Page from "../../layouts/Page";
import MusicLandHeader from "../../components/MusicLandHeader";
import { goBack, navigate } from "../../navigation/NavigationService";
import { gutters, Palette } from "../../styles";
import { gutters } from "../../styles";
import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import { Routes } from "../../navigation";
import CustomInput from "./components/CustomInput";
import { FONT_FAMILY } from "../../styles/Fonts";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
import { useRoute } from "@react-navigation/native";
import firebase from "../../config/firebase";
import firebase, { projectsRef } from "../../config/firebase";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
const Lyrics = ({ navigation }) => {
@@ -20,17 +19,30 @@ const Lyrics = ({ navigation }) => {
const lyricsData = route?.params?.lyricsData;
const config = route?.params?.config;
const selections = route?.params?.selections;
const hasLyrics = !!route?.params?.hasLyrics;
const { setIsLoading } = useMinuit();
const initial = useMemo(() => {
const title = lyricsData?.title || "";
const aiSections = Array.isArray(lyricsData?.lyrics) ? lyricsData.lyrics : [];
const aiSections = Array.isArray(lyricsData?.lyrics)
? lyricsData.lyrics
: [];
// Respecter l'ordre de la structure choisie si disponible
const targetStructure = Array.isArray(config?.structure)
? config.structure.map((t) => (t || "").toLowerCase())
: null;
if (aiSections.length && targetStructure && aiSections.length === targetStructure.length) {
return { title, sections: aiSections.map((s) => ({ type: (s?.type || "").toLowerCase(), lyrics: s?.lyrics || "" })) };
if (
aiSections.length &&
targetStructure &&
aiSections.length === targetStructure.length
) {
return {
title,
sections: aiSections.map((s) => ({
type: (s?.type || "").toLowerCase(),
lyrics: s?.lyrics || "",
})),
};
}
// Sinon, créer à partir de la structure
if (targetStructure && targetStructure.length) {
@@ -77,9 +89,23 @@ const Lyrics = ({ navigation }) => {
const onValidate = useCallback(async () => {
try {
await setIsLoading(true);
const titleTrimmed = (titleValue || "").trim();
if (!titleTrimmed) {
Alert.alert("Titre manquant", "Veuillez renseigner un titre.");
return;
}
const invalid = (sections || []).some((s) => !(s?.lyrics || "").trim());
if (invalid) {
Alert.alert(
"Champs incomplets",
"Chaque couplet et refrain doit contenir du texte.",
);
return;
}
const user = firebase.auth().currentUser;
const payload = {
title: titleValue?.trim() || "",
title: titleTrimmed,
titleLower: titleTrimmed.toLowerCase(),
lyrics: (sections || []).map((s) => ({
type: (s?.type || "").toLowerCase(),
lyrics: s?.lyrics || "",
@@ -89,9 +115,10 @@ const Lyrics = ({ navigation }) => {
userId: user ? user.uid : null,
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
hasLyrics,
};
await firebase.firestore().collection("projects").add(payload);
navigate(Routes.Studio);
const { id: projectId } = await projectsRef.add(payload);
navigate(Routes.FlowSelection, { projectId });
} catch (e) {
console.log(e);
Alert.alert("Erreur", "Échec de l'enregistrement dans le projet.");
@@ -157,10 +184,12 @@ const Lyrics = ({ navigation }) => {
gap: 12,
}}
>
<BorderGradientButton
title="Générer d'autre paroles"
onPress={regenerate}
/>
{!hasLyrics && (
<BorderGradientButton
title="Générer d'autres paroles"
onPress={regenerate}
/>
)}
<GradientButton title="Valider" onPress={onValidate} />
</View>
</Page>
+7 -5
View File
@@ -1,13 +1,15 @@
import { View, Text, StyleSheet, FlatList, Pressable } from "react-native";
import { View, Text, StyleSheet, FlatList } from "react-native";
import React, { useState } from "react";
import CreateLyricsHeader from "./components/CreateLyricsHeader";
import { BlurView } from "expo-blur";
import { GOALS, SONG_STRUCTURE } from "../../data/data";
import { Palette, Style } from "../../styles";
import { SONG_STRUCTURE } from "../../data/data";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
const SongStructure = ({ selected: selectedProp, setSelected: setSelectedProp }) => {
const SongStructure = ({
selected: selectedProp,
setSelected: setSelectedProp,
}) => {
const [internalSelected, setInternalSelected] = useState(null);
const selected = selectedProp ?? internalSelected;
const setSelected = setSelectedProp ?? setInternalSelected;
+6 -1
View File
@@ -8,7 +8,12 @@ import CustomInput from "./components/CustomInput";
import { FONT_FAMILY } from "../../styles/Fonts";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
const SongStyle = ({ selected: selectedProp, setSelected: setSelectedProp, otherStyle, setOtherStyle }) => {
const SongStyle = ({
selected: selectedProp,
setSelected: setSelectedProp,
otherStyle,
setOtherStyle,
}) => {
const [internalSelected, setInternalSelected] = useState(null);
const selected = selectedProp ?? internalSelected;
const setSelected = setSelectedProp ?? setInternalSelected;
+8 -2
View File
@@ -13,7 +13,7 @@ const WritingLyrics = () => {
return (
<Page headerType="NONE">
<Image source={ai.nathalie} style={styles.img} resizeMode="contain" />
<MusicLandHeader showSkip onPressBack={goBack} progress={9} />
<MusicLandHeader onPressBack={goBack} progress={9} />
<View
style={{ flex: 1, position: "relative", justifyContent: "flex-end" }}
>
@@ -24,7 +24,13 @@ const WritingLyrics = () => {
gap: 12,
}}
>
<BorderGradientButton />
<BorderGradientButton
onPress={() => {
navigate(Routes.CreateLyricsWithAi, {
hasLyrics: true,
});
}}
/>
<GradientButton
title="Écrire des paroles avec une IA"
onPress={() => navigate(Routes.CreateLyricsWithAi)}