fix tickets add reort mail and design fixes

This commit is contained in:
Thomas Demirdjian
2025-10-29 15:36:12 +01:00
parent d1be3c19a2
commit ce285881ca
35 changed files with 1069 additions and 202 deletions
+35 -51
View File
@@ -8,8 +8,7 @@ import MusicLandHeader from "../../components/MusicLandHeader";
import { OTHER_OBJECTIVE_OPTION, OTHER_STYLE_OPTION } from "../../data/data";
import useLayoutType from "../../hooks/useLayoutType";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService";
import { goBack } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { gutters } from "../../styles";
import { sanitizeStructureList } from "../../utils/songStructure";
@@ -23,13 +22,13 @@ import SongStyle from "./SongStyle";
import SongTo from "./SongTo";
import SpecificityContext from "./SpecificityContext";
const { width: windowWidth } = Dimensions.get("window");
const MAX_STEP_INDEX = 8;
const CreateLyricsWithAi = () => {
const { isWeb } = useLayoutType();
const { selectedProject, updateProjectData } = useUser();
const hasLyrics = selectedProject?.hasLyrics === true;
const { selectedProject } = useUser();
const scrollRef = useRef(null);
const [selectedIndex, setSelectedIndex] = useState(hasLyrics ? 5 : 0);
const [selectedIndex, setSelectedIndex] = useState(0);
const [progress, setProgress] = useState(16);
const [parentLayout, setparentLayout] = useState(null);
const containerWidth = windowWidth;
@@ -188,13 +187,6 @@ const CreateLyricsWithAi = () => {
}
}, [structure]);
// Si déjà des paroles, forcer l'accès à partir de l'étape 5 et ignorer 0-4
React.useEffect(() => {
if (hasLyrics && selectedIndex < 5) {
setSelectedIndex(5);
}
}, [hasLyrics]);
const lyricsConfig = useMemo(() => {
const resolvedObjective = shouldUseOtherObjective
? persistedOtherObjective || undefined
@@ -297,37 +289,7 @@ const CreateLyricsWithAi = () => {
]);
const onPressNext = async () => {
// Si l'utilisateur a déjà des paroles et vient de finir CustomizeSongStructure (index 6),
// sauter Rhymes et CreatingLyrics et aller directement sur Lyrics
if (hasLyrics && selectedIndex === 6) {
const chosenStructure = sanitizeStructureList(
Array.isArray(customStructure) && customStructure.length > 0
? customStructure
: Array.isArray(parsedStructure)
? parsedStructure
: []
);
await updateProjectData({
config: { structure: chosenStructure },
selections: {
objective,
otherObjective: persistedOtherObjective,
context,
emotion,
style,
otherStyle: persistedOtherStyle,
audience,
structure,
parsedStructure,
customStructure: sanitizeStructureList(customStructure),
rhymes,
},
hasLyrics: true,
});
navigate(Routes.Lyrics);
return;
}
setSelectedIndex((idx) => idx + 1);
setSelectedIndex((idx) => Math.min(idx + 1, MAX_STEP_INDEX));
};
const onPressBack = () => {
@@ -344,14 +306,36 @@ const CreateLyricsWithAi = () => {
// 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 (_) {}
const nextProgress = Math.max(0, Math.min(100, 16 + selectedIndex * 9));
setProgress(nextProgress);
const ref = scrollRef.current;
if (!ref || typeof ref.scrollToIndex !== "function") {
return;
}
let cancelled = false;
const attemptScroll = (retries = 0) => {
if (cancelled) return;
try {
ref.scrollToIndex({ index: selectedIndex, animated: true });
} catch (error) {
if (retries < 3) {
setTimeout(() => attemptScroll(retries + 1), 40);
} else {
console.warn("[CreateLyricsWithAi] scrollToIndex failed", {
index: selectedIndex,
error,
});
}
}
};
attemptScroll();
return () => {
cancelled = true;
};
}, [selectedIndex, parentLayout?.height]);
return (
+6 -43
View File
@@ -6,8 +6,7 @@ import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
import { OTHER_OBJECTIVE_OPTION, OTHER_STYLE_OPTION } from "../../data/data";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService";
import { goBack } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { gutters } from "../../styles";
import { sanitizeStructureList } from "../../utils/songStructure";
@@ -21,10 +20,11 @@ import SongStyle from "./SongStyle";
import SongTo from "./SongTo";
import SpecificityContext from "./SpecificityContext";
const MAX_STEP_INDEX = 8;
const CreateLyricsWithAi = () => {
const { selectedProject, updateProjectData } = useUser();
const hasLyrics = selectedProject?.hasLyrics === true;
const [selectedIndex, setSelectedIndex] = useState(hasLyrics ? 5 : 0);
const { selectedProject } = useUser();
const [selectedIndex, setSelectedIndex] = useState(0);
// Collected state across steps
const [objective, setObjective] = useState(null); // from Goals list
@@ -187,13 +187,6 @@ const CreateLyricsWithAi = () => {
}
}, [structure]);
// Si déjà des paroles, forcer l'accès à partir de l'étape 5 et ignorer 0-4
React.useEffect(() => {
if (hasLyrics && selectedIndex < 5) {
setSelectedIndex(5);
}
}, [hasLyrics]);
const progress = useMemo(
() => 16 + selectedIndex * 9,
[selectedIndex]
@@ -301,37 +294,7 @@ const CreateLyricsWithAi = () => {
]);
const onPressNext = async () => {
// Si l'utilisateur a déjà des paroles et vient de finir CustomizeSongStructure (index 6),
// sauter Rhymes et CreatingLyrics et aller directement sur Lyrics
if (hasLyrics && selectedIndex === 6) {
const chosenStructure = sanitizeStructureList(
Array.isArray(customStructure) && customStructure.length > 0
? customStructure
: Array.isArray(parsedStructure)
? parsedStructure
: []
);
await updateProjectData({
config: { structure: chosenStructure },
selections: {
objective,
otherObjective: persistedOtherObjective,
context,
emotion,
style,
otherStyle: persistedOtherStyle,
audience,
structure,
parsedStructure,
customStructure: sanitizeStructureList(customStructure),
rhymes,
},
hasLyrics: true,
});
navigate(Routes.Lyrics);
return;
}
setSelectedIndex((idx) => idx + 1);
setSelectedIndex((idx) => Math.min(idx + 1, MAX_STEP_INDEX));
};
const onPressBack = () => {
+3 -2
View File
@@ -3,6 +3,7 @@ import { View } from "react-native";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
import ListSelection from "../../components/ListSelection/ListSelection";
import { EMOTION_CONVEY } from "../../data/data";
import { strings } from "../../constants/strings";
import CreateLyricsHeader from "./components/CreateLyricsHeader";
const EmotionConvey = ({
@@ -19,8 +20,8 @@ const EmotionConvey = ({
return (
<View style={{ flex: 1, gap: 10, marginTop: 16 }}>
<CreateLyricsHeader
title={`Quelle émotion veux-tu\ntransmettre ?`}
subTitle="Sélectionne une seule intention émotionnelle."
title={strings.writing.steps.emotionTitle}
subTitle={strings.writing.steps.emotionSubtitle}
/>
<View
+22 -5
View File
@@ -1,8 +1,9 @@
import React, { useMemo, useState } from "react";
import { Platform, ScrollView, StyleSheet, View } from "react-native";
import { Platform, ScrollView, StyleSheet, Text, View } from "react-native";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
import ListSelection from "../../components/ListSelection/ListSelection";
import { GOALS, OTHER_OBJECTIVE_OPTION } from "../../data/data";
import { strings } from "../../constants/strings";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import CreateLyricsHeader from "./components/CreateLyricsHeader";
@@ -39,9 +40,14 @@ const Goals = ({
<ScrollView contentContainerStyle={{ flex: 1, gap: 16, marginTop: 16 }}>
<View style={{ gap: 10 }}>
<CreateLyricsHeader
title="Quel est le contexte ?"
subTitle="Besoin d'idées ? Pense à un anniversaire, une équipe de sport ou ton entreprise."
title={strings.writing.steps.contextTitle}
subTitle={strings.writing.steps.contextSubtitle}
/>
<View style={styles.examplesContainer}>
<Text style={styles.examplesText}>
{strings.writing.steps.contextExamples}
</Text>
</View>
<ItemContainer>
<ListSelection
options={goalsOptions}
@@ -55,8 +61,8 @@ const Goals = ({
</ItemContainer>
</View>
<CustomInput
label="As-tu un autre objectif ?"
placeholder="Décrire lobjectif"
label={strings.writing.labels.otherObjective}
placeholder={strings.writing.labels.otherObjectivePlaceholder}
value={otherObjective}
setValue={handleOtherObjectiveChange}
height={Platform.OS === "web" ? 160 : undefined}
@@ -95,4 +101,15 @@ const styles = StyleSheet.create({
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
},
examplesContainer: {
backgroundColor: Palette.ultraLightWhite,
borderRadius: 12,
padding: 12,
},
examplesText: {
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
lineHeight: 20,
},
});
+21 -17
View File
@@ -8,6 +8,7 @@ import GradientButton from "../../components/GradientButton";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
import MusicLandHeader from "../../components/MusicLandHeader";
import firebase, { projectsRef } from "../../config/firebase";
import { strings } from "../../constants/strings";
import { isWeb } from "../../hooks/useLayoutType";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
@@ -363,24 +364,17 @@ const Lyrics = ({ navigation }) => {
}}
>
<View style={styles.headerContainer}>
<Text style={styles.headerTitle}>Proposition de paroles</Text>
<Text style={styles.instructions}>
Relis attentivement la proposition générée avant de valider.
<Text style={styles.headerTitle}>
{strings.writing.lyrics.title}
</Text>
<Text style={styles.instructions}>
<Text style={styles.instructionsHighlight}>
Personnalisation :
</Text>{" "}
modifie le titre et chaque section pour que les paroles te
ressemblent.
</Text>
<Text style={styles.instructions}>
<Text style={styles.instructionsHighlight}>
Nouvelle génération :
</Text>{" "}
appuie sur "Générer d'autres paroles" si tu souhaites une autre
suggestion.
{strings.writing.lyrics.instructions}
</Text>
<View style={styles.personalizationBanner}>
<Text style={styles.personalizationText}>
{strings.writing.lyrics.personalizationBanner}
</Text>
</View>
</View>
<CustomInput
label="Titre"
@@ -489,8 +483,18 @@ const styles = StyleSheet.create({
fontSize: 14,
lineHeight: 20,
},
instructionsHighlight: {
fontFamily: FONT_FAMILY.InterSemiBold,
personalizationBanner: {
backgroundColor: Palette.ultraLightWhite,
borderRadius: 12,
paddingVertical: 10,
paddingHorizontal: 12,
marginTop: 4,
},
personalizationText: {
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 14,
lineHeight: 20,
},
instrumentalBlock: {
padding: 16,
+65 -21
View File
@@ -1,5 +1,5 @@
import React from "react";
import { Image, StyleSheet, View } from "react-native";
import { Image, StyleSheet, Text, View } from "react-native";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import { ai, background, icons } from "../../assets";
import BorderGradientButton from "../../components/BorderGradientButton";
@@ -10,22 +10,24 @@ import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService";
import { useUserData } from "../../providers/UserDataProvider";
import { gutters } from "../../styles";
import { strings } from "../../constants/strings";
import { Palette, gutters } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
const WritingLyrics = () => {
const { createNewProject, selectedProject, updateProjectData } =
useUserData();
const { setIsLoading } = useMinuit();
async function createAndNavigate({ hasLyrics = false }) {
const startWriting = React.useCallback(async () => {
try {
await setIsLoading(true);
if (selectedProject) {
updateProjectData({ hasLyrics });
updateProjectData({ hasLyrics: false });
setTimeout(() => navigate(Routes.CreateLyricsWithAi), 500);
return;
}
const newProjectId = await createNewProject({ hasLyrics });
const newProjectId = await createNewProject({ hasLyrics: false });
if (!newProjectId) {
return;
}
@@ -35,7 +37,7 @@ const WritingLyrics = () => {
} finally {
await setIsLoading(false);
}
}
}, [createNewProject, navigate, selectedProject, setIsLoading, updateProjectData]);
return (
<Page
@@ -48,27 +50,34 @@ const WritingLyrics = () => {
progress={9}
logo={icons.musicLandWriting}
/>
<View
style={{ flex: 1, position: "relative", justifyContent: "flex-end" }}
>
<View
style={{
paddingBottom: gutters * 2,
paddingHorizontal: gutters,
gap: 12,
}}
>
<View style={styles.contentWrapper}>
<View style={styles.heroContainer}>
<Text style={styles.heroTitle}>
{strings.writing.onboarding.heroTitle}
</Text>
<Text style={styles.heroSubtitle}>
{strings.writing.onboarding.heroSubtitle}
</Text>
</View>
<View style={styles.ctaGroup}>
<GradientButton
maxWidth={500}
size="large"
maxWidth={520}
containerStyle={{ alignSelf: "center", width: "100%" }}
title="Écrire des paroles avec une IA"
onPress={() => createAndNavigate({ hasLyrics: false })}
title={strings.writing.onboarding.primaryCta}
onPress={startWriting}
textStyle={{ fontFamily: FONT_FAMILY.InterSemiBold }}
/>
<BorderGradientButton
maxWidth={500}
onPress={() => createAndNavigate({ hasLyrics: true })}
size="small"
title={strings.writing.onboarding.secondaryCta}
disabled
containerStyle={{ alignSelf: "center", width: "100%" }}
maxWidth={360}
/>
<Text style={styles.secondaryNote}>
{strings.writing.onboarding.secondaryNote}
</Text>
</View>
</View>
</Page>
@@ -85,4 +94,39 @@ const styles = StyleSheet.create({
bottom: -40,
right: -30,
},
contentWrapper: {
flex: 1,
justifyContent: "space-between",
paddingTop: gutters * 6,
paddingBottom: gutters * 2,
paddingHorizontal: gutters,
gap: 32,
},
heroContainer: {
gap: 12,
maxWidth: 560,
},
heroTitle: {
fontSize: 28,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
},
heroSubtitle: {
fontSize: 16,
lineHeight: 24,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
opacity: 0.88,
},
ctaGroup: {
gap: 12,
},
secondaryNote: {
textAlign: "center",
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 14,
opacity: 0.85,
fontStyle: "italic",
},
});