This commit is contained in:
Thomas Demirdjian
2025-10-03 14:09:47 +02:00
parent 1907449dee
commit bfca92f9e0
10 changed files with 265 additions and 305 deletions
+1
View File
@@ -18,3 +18,4 @@ exports.cover = require("./src/cover");
exports.projects = require("./src/project"); exports.projects = require("./src/project");
exports.thumbnail = require("./src/thumbnail"); exports.thumbnail = require("./src/thumbnail");
exports.upload = require("./src/upload"); exports.upload = require("./src/upload");
exports.algolia = require("./src/algolia");
+26
View File
@@ -0,0 +1,26 @@
const { onRequest } = require("firebase-functions/v2/https");
exports.algoliaTransformProjectData = onRequest(
{ region: "europe-west1" },
(req, res) => {
const payload = req.body.data;
const objectID = payload.objectID;
try {
const flat = { ...payload };
delete flat["musicTimestamps"];
console.log(`Change in ${payload.objectID}`);
console.log(flat);
// Ton object final doit contenir "objectID"
const result = {
objectID,
...flat,
};
res.send({ result });
} catch (e) {
console.log(`Error ${objectID}:`, e.message);
res.status(500).end();
}
},
);
+14
View File
@@ -33,6 +33,20 @@ exports.onProjectUpdate = onDocumentWritten(
batch.delete(doc.ref); batch.delete(doc.ref);
}); });
await batch.commit(); await batch.commit();
} else {
if (previousData) {
// edit
if (!previousData?.songUrl && currentData?.songUrl) {
await refList.projects.doc(projectId).update({
hasSong: true,
});
}
if (!previousData?.playbackUrl && currentData?.playbackUrl) {
await refList.projects.doc(projectId).update({
hasPlayback: true,
});
}
}
} }
} catch (e) { } catch (e) {
console.log(e); console.log(e);
+14
View File
@@ -19,3 +19,17 @@ export const GOOGLE_IOS_CLIENT_ID =
// Android client ID (client_type 1 from google-services.json) // Android client ID (client_type 1 from google-services.json)
export const GOOGLE_ANDROID_CLIENT_ID = export const GOOGLE_ANDROID_CLIENT_ID =
"305598753437-qck73beo37mi04qlautgafbpdl3ccaoh.apps.googleusercontent.com"; "305598753437-qck73beo37mi04qlautgafbpdl3ccaoh.apps.googleusercontent.com";
const AlgoliaAppId = "38AHXYR8S1";
const AlgoliaSearchKey = "2ac15d7b8b5e6490b53821067e4a0f03";
export const AlgoliaUserConfig = {
index: "users",
projectID: AlgoliaAppId,
publicKey: AlgoliaSearchKey,
};
export const AlgoliaProjectConfig = {
index: "projects",
projectID: AlgoliaAppId,
publicKey: AlgoliaSearchKey,
};
+30 -209
View File
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from "react"; import { useState } from "react";
import { useGlobal } from "reactn"; import useAlgoliaSearch from "react-native-minuit/src/hooks/useAlgoliaSearch";
import { usersRef, projectsRef } from "../config/firebase"; import { AlgoliaUserConfig, AlgoliaProjectConfig } from "../data/keys";
import useDataFromRef from "./useDataFromRef"; import { useUserData } from "../providers/UserDataProvider";
const batchSizes = { const batchSizes = {
users: 5, users: 5,
@@ -11,221 +11,42 @@ const batchSizes = {
const useSearch = () => { const useSearch = () => {
const [selected, setSelected] = useState(null); const [selected, setSelected] = useState(null);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const queryText = useMemo(() => search.trim(), [search]); const { currentUID } = useUserData();
const queryLower = useMemo(
() => (search || "").toLowerCase().trim(),
[search],
);
const [currentUID] = useGlobal("currentUID");
const [autoLoads, setAutoLoads] = useState(0);
const usersQueryRef = useMemo(() => { const { hits: users, loading: userLoading } = useAlgoliaSearch({
try { query: search,
const lower = queryLower; algoliaObject: AlgoliaUserConfig,
if (lower) { batch: batchSizes.users,
return usersRef searchParams: {
.orderBy("userNameLower") filters: `NOT objectID:${currentUID}`,
.startAt(lower) },
.endAt(lower + "\uf8ff");
}
return usersRef.orderBy("userNameLower");
} catch (_error) {
return null;
}
}, [queryLower]);
const projectsQueryRefLower = useMemo(() => {
try {
const lower = queryText ? queryText.toLowerCase() : "";
if (lower) {
return projectsRef
.orderBy("titleLower")
.startAt(lower)
.endAt(lower + "\uf8ff");
}
return null;
} catch (_error) {
return null;
}
}, [queryText]);
const projectsQueryRefCase = useMemo(() => {
try {
const raw = queryText || "";
if (raw) {
return projectsRef
.orderBy("title")
.startAt(raw)
.endAt(raw + "\uf8ff");
}
return null;
} catch (_error) {
return null;
}
}, [queryText]);
const {
data: userResults = [],
loading: usersLoading,
loadMore: loadMoreUsers,
} = useDataFromRef({
ref: usersQueryRef,
simpleRef: false,
listener: false,
condition: !!usersQueryRef,
refreshArray: [queryLower],
usePagination: true,
batchSize: batchSizes.users,
}); });
const { hits: playbacks, loading: playbackLoading } = useAlgoliaSearch({
const filteredUsers = useMemo(() => { query: search,
const arr = Array.isArray(userResults) ? userResults : []; algoliaObject: AlgoliaProjectConfig,
const matchingUsers = queryLower batch: batchSizes.projects,
? arr.filter((user) => { searchParams: {
const nameLower = ( filters: `hasPlayback:true AND hasSong:true`,
user?.userNameLower || },
user?.userName ||
""
).toLowerCase();
return nameLower.includes(queryLower);
})
: arr;
const seen = new Set();
const unique = [];
for (const user of matchingUsers) {
const id = user?.id;
if (!id || seen.has(id)) continue;
if (id === currentUID || user?.userID === currentUID) continue;
seen.add(id);
unique.push(user);
}
return unique;
}, [userResults, queryLower, currentUID]);
const {
data: projectResultsLower = [],
loading: projectsLoadingLower,
loadMore: loadMoreProjectsLower,
} = useDataFromRef({
ref: projectsQueryRefLower,
simpleRef: false,
listener: false,
condition: !!projectsQueryRefLower,
refreshArray: [queryText],
usePagination: true,
batchSize: batchSizes.projects,
}); });
const { hits: musics, loading: musicLoading } = useAlgoliaSearch({
const { query: search,
data: projectResultsCase = [], algoliaObject: AlgoliaProjectConfig,
loading: projectsLoadingCase, batch: batchSizes.projects,
loadMore: loadMoreProjectsCase, searchParams: {
} = useDataFromRef({ filters: `hasPlayback:false AND hasSong:true`,
ref: projectsQueryRefCase, },
simpleRef: false,
listener: false,
condition: !!projectsQueryRefCase,
refreshArray: [queryText],
usePagination: true,
batchSize: batchSizes.projects,
}); });
const {
data: projectResultsDefault = [],
loading: projectsLoadingDefault,
loadMore: loadMoreProjectsDefault,
} = useDataFromRef({
ref: !queryText ? projectsRef.orderBy("updatedAt", "desc") : null,
simpleRef: false,
listener: false,
condition: !queryText,
refreshArray: [queryText],
usePagination: true,
batchSize: batchSizes.projects,
});
const projectsLoading = queryText
? projectsLoadingLower || projectsLoadingCase
: projectsLoadingDefault;
const loadMoreProjects = () => {
if (queryText) {
loadMoreProjectsLower?.();
loadMoreProjectsCase?.();
} else {
loadMoreProjectsDefault?.();
}
};
const filteredProjects = useMemo(() => {
const lowerMatches = Array.isArray(projectResultsLower)
? projectResultsLower
: [];
const caseMatches = Array.isArray(projectResultsCase)
? projectResultsCase
: [];
const defaultProjects = Array.isArray(projectResultsDefault)
? projectResultsDefault
: [];
const arr = queryText ? [...lowerMatches, ...caseMatches] : defaultProjects;
const seen = new Set();
const unique = [];
for (const project of arr) {
const id = project?.id;
if (!id || seen.has(id)) continue;
seen.add(id);
unique.push(project);
}
return unique;
}, [
queryText,
projectResultsLower,
projectResultsCase,
projectResultsDefault,
]);
useEffect(() => {
setAutoLoads(0);
}, [queryText]);
useEffect(() => {
if (
filteredUsers.length < batchSizes.users &&
usersQueryRef &&
!usersLoading
) {
if (autoLoads < 3) {
setAutoLoads((previous) => previous + 1);
loadMoreUsers?.();
}
}
}, [
filteredUsers.length,
usersLoading,
usersQueryRef,
autoLoads,
loadMoreUsers,
]);
const toggleSelected = (item) => {
setSelected((prev) => (prev === item ? null : item));
};
return { return {
search, search,
setSearch, setSearch,
queryText,
queryLower,
selected, selected,
setSelected, setSelected,
toggleSelected, users,
filteredUsers, musics,
usersLoading, playbacks,
loadMoreUsers, loading: userLoading || musicLoading || playbackLoading,
filteredProjects,
projectsLoading,
loadMoreProjects,
}; };
}; };
+36 -13
View File
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useRef, useState } from "react"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Image, Platform, ScrollView, Text, View } from "react-native"; import { Image, Platform, ScrollView, Text, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions"; import { responsiveHeight } from "react-native-responsive-dimensions";
import { background, icons } from "../../assets"; import { background, icons } from "../../assets";
@@ -50,15 +50,38 @@ const Library = () => {
setSearch, setSearch,
selected, selected,
setSelected, setSelected,
toggleSelected, users = [],
filteredProjects, musics = [],
projectsLoading, playbacks = [],
loadMoreProjects, loading,
filteredUsers,
usersLoading,
loadMoreUsers,
} = useSearch(); } = useSearch();
const toggleSelected = useCallback(
(value) => {
setSelected((previous) => (previous === value ? null : value));
},
[setSelected],
);
const musicResults = useMemo(
() => (Array.isArray(musics) ? musics : []),
[musics],
);
const playbackResults = useMemo(
() => (Array.isArray(playbacks) ? playbacks : []),
[playbacks],
);
const userResults = useMemo(
() => (Array.isArray(users) ? users : []),
[users],
);
const musicsLoading = loading;
const playbacksLoading = loading;
const usersLoading = loading;
const [dropdownVisible, setDropdownVisible] = useState(false); const [dropdownVisible, setDropdownVisible] = useState(false);
const searchWrapperRef = useRef(null); const searchWrapperRef = useRef(null);
@@ -152,12 +175,12 @@ const Library = () => {
/> />
<SearchResultsList <SearchResultsList
selected={selected} selected={selected}
filteredProjects={filteredProjects} musics={musicResults}
projectsLoading={projectsLoading} musicsLoading={musicsLoading}
loadMoreProjects={loadMoreProjects} playbacks={playbackResults}
filteredUsers={filteredUsers} playbacksLoading={playbacksLoading}
users={userResults}
usersLoading={usersLoading} usersLoading={usersLoading}
loadMoreUsers={loadMoreUsers}
onResultSelected={closeDropdown} onResultSelected={closeDropdown}
style={{ flex: 0, maxHeight: 420 }} style={{ flex: 0, maxHeight: 420 }}
/> />
+37 -13
View File
@@ -1,4 +1,4 @@
import React from "react"; import React, { useCallback, useMemo } from "react";
import { View } from "react-native"; import { View } from "react-native";
import { background } from "../../assets"; import { background } from "../../assets";
import useSearch from "../../hooks/useSearch"; import useSearch from "../../hooks/useSearch";
@@ -12,15 +12,39 @@ const Research = () => {
search, search,
setSearch, setSearch,
selected, selected,
toggleSelected, setSelected,
filteredProjects, users = [],
projectsLoading, musics = [],
loadMoreProjects, playbacks = [],
filteredUsers, loading,
usersLoading,
loadMoreUsers,
} = useSearch(); } = useSearch();
const toggleSelected = useCallback(
(value) => {
setSelected((previous) => (previous === value ? null : value));
},
[setSelected],
);
const musicResults = useMemo(
() => (Array.isArray(musics) ? musics : []),
[musics],
);
const playbackResults = useMemo(
() => (Array.isArray(playbacks) ? playbacks : []),
[playbacks],
);
const userResults = useMemo(
() => (Array.isArray(users) ? users : []),
[users],
);
const musicsLoading = loading;
const playbacksLoading = loading;
const usersLoading = loading;
return ( return (
<Page <Page
headerType="NAVIGATION" headerType="NAVIGATION"
@@ -39,12 +63,12 @@ const Research = () => {
/> />
<SearchResultsList <SearchResultsList
selected={selected} selected={selected}
filteredProjects={filteredProjects} musics={musicResults}
projectsLoading={projectsLoading} musicsLoading={musicsLoading}
loadMoreProjects={loadMoreProjects} playbacks={playbackResults}
filteredUsers={filteredUsers} playbacksLoading={playbacksLoading}
users={userResults}
usersLoading={usersLoading} usersLoading={usersLoading}
loadMoreUsers={loadMoreUsers}
/> />
</View> </View>
</Page> </Page>
@@ -1,6 +1,6 @@
import { Image as ExpoImage } from "expo-image";
import React, { useState } from "react"; import React, { useState } from "react";
import { Pressable, ScrollView, Text, View } from "react-native"; import { Pressable, ScrollView, Text, View } from "react-native";
import { Image as ExpoImage } from "expo-image";
import { img } from "../../../assets"; import { img } from "../../../assets";
import MoreMenu from "../../../components/MoreMenu"; import MoreMenu from "../../../components/MoreMenu";
import { Routes } from "../../../navigation"; import { Routes } from "../../../navigation";
@@ -28,12 +28,12 @@ const EmptyText = ({ text }) => (
const SearchResultsList = ({ const SearchResultsList = ({
selected, selected,
filteredProjects, musics = [],
projectsLoading, musicsLoading = false,
loadMoreProjects, playbacks = [],
filteredUsers, playbacksLoading = false,
usersLoading, users = [],
loadMoreUsers, usersLoading = false,
onResultSelected, onResultSelected,
contentContainerStyle, contentContainerStyle,
style, style,
@@ -52,14 +52,23 @@ const SearchResultsList = ({
onResultSelected?.(); onResultSelected?.();
}; };
const shouldShowMusics =
(!selected && (musics.length > 0 || musicsLoading)) || selected === "Musiques";
const shouldShowPlaybacks =
(!selected && (playbacks.length > 0 || playbacksLoading)) ||
selected === "Playbacks";
const shouldShowUsers =
(!selected && (users.length > 0 || usersLoading)) || selected === "Profils";
return ( return (
<View style={[{ flex: 1 }, style]}> <View style={[{ flex: 1 }, style]}>
<ScrollView <ScrollView
contentContainerStyle={{ paddingTop: 2, ...contentContainerStyle }} contentContainerStyle={{ paddingTop: 2, ...contentContainerStyle }}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
> >
{((!selected && (filteredProjects.length > 0 || projectsLoading)) || {shouldShowMusics && (
selected === "Musiques") && (
<View style={{ paddingHorizontal: 2 }}> <View style={{ paddingHorizontal: 2 }}>
<CreateLyricsHeader <CreateLyricsHeader
gradientProps={{ gradientProps={{
@@ -67,10 +76,10 @@ const SearchResultsList = ({
end: { x: 0, y: 1 }, end: { x: 0, y: 1 },
}} }}
> >
{filteredProjects.length > 0 && ( {musics.length > 0 && (
<View style={{ gap: 10 }}> <View style={{ gap: 10 }}>
{Array.isArray(filteredProjects) && {Array.isArray(musics) &&
filteredProjects.slice(0, 6).map((project) => ( musics.slice(0, 6).map((project) => (
<MusicCard <MusicCard
key={project.id} key={project.id}
title={project?.title || "Sans titre"} title={project?.title || "Sans titre"}
@@ -85,46 +94,91 @@ const SearchResultsList = ({
setShowMenu( setShowMenu(
(previous) => (previous) =>
!previous || !previous ||
positionTop?.top !== (menuPosition?.top ?? null) positionTop?.top !== (menuPosition?.top ?? null),
); );
}} }}
/> />
))} ))}
{/* {(filteredProjects.length >= 6 || projectsLoading) && ( {musicsLoading && (
<Pressable <Text
onPress={loadMoreProjects} style={{
style={{ alignSelf: "center", marginTop: 6 }} fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
alignSelf: "center",
}}
> >
<Text {"Chargement…"}
style={{ </Text>
fontSize: 12, )}
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
opacity: projectsLoading ? 0.6 : 1,
}}
>
{projectsLoading ? "Chargement…" : "Charger plus"}
</Text>
</Pressable>
)} */}
</View> </View>
)} )}
{filteredProjects.length === 0 && !projectsLoading && ( {musics.length === 0 && !musicsLoading && (
<EmptyText text={"Aucune musique trouvée"} /> <EmptyText text={"Aucune musique trouvée"} />
)} )}
</CreateLyricsHeader> </CreateLyricsHeader>
</View> </View>
)} )}
{((!selected && (filteredUsers.length > 0 || usersLoading)) || {shouldShowPlaybacks && (
selected === "Profils") && ( <View style={{ paddingHorizontal: 2, paddingTop: 10 }}>
<CreateLyricsHeader
gradientProps={{
start: { x: 0, y: 0 },
end: { x: 0, y: 1 },
}}
>
{playbacks.length > 0 && (
<View style={{ gap: 10 }}>
{Array.isArray(playbacks) &&
playbacks.slice(0, 6).map((project) => (
<MusicCard
key={project.id}
title={project?.title || "Sans titre"}
subtitle={"MusicLand"}
imageUri={project?.coverUrl || null}
projectId={project?.id}
likedBy={project?.likedBy || []}
onPress={() => handleProjectPress(project.id)}
onPressMore={(positionTop) => {
setSelectedProjectId(project.id);
setMenuPosition(positionTop);
setShowMenu(
(previous) =>
!previous ||
positionTop?.top !== (menuPosition?.top ?? null),
);
}}
/>
))}
{playbacksLoading && (
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
alignSelf: "center",
}}
>
{"Chargement…"}
</Text>
)}
</View>
)}
{playbacks.length === 0 && !playbacksLoading && (
<EmptyText text={"Aucun playback trouvé"} />
)}
</CreateLyricsHeader>
</View>
)}
{shouldShowUsers && (
<View style={{ paddingHorizontal: 2, paddingTop: 10 }}> <View style={{ paddingHorizontal: 2, paddingTop: 10 }}>
<CreateLyricsHeader <CreateLyricsHeader
gradientProps={{ start: { x: 0, y: 0 }, end: { x: 0, y: 1 } }} gradientProps={{ start: { x: 0, y: 0 }, end: { x: 0, y: 1 } }}
> >
{filteredUsers.length > 0 && ( {users.length > 0 && (
<View style={{ gap: 8 }}> <View style={{ gap: 8 }}>
{Array.isArray(filteredUsers) && {Array.isArray(users) &&
filteredUsers.slice(0, 5).map((user) => ( users.slice(0, 5).map((user) => (
<Pressable <Pressable
style={{ gap: 14, ...Style.containerRow }} style={{ gap: 14, ...Style.containerRow }}
key={user.id} key={user.id}
@@ -163,40 +217,26 @@ const SearchResultsList = ({
</Text> </Text>
</Pressable> </Pressable>
))} ))}
{/* {(filteredUsers.length >= 5 || usersLoading) && ( {usersLoading && (
<Pressable <Text
onPress={loadMoreUsers} style={{
style={{ alignSelf: "center", marginTop: 6 }} fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
alignSelf: "center",
}}
> >
<Text {"Chargement…"}
style={{ </Text>
fontSize: 12, )}
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
opacity: usersLoading ? 0.6 : 1,
}}
>
{usersLoading ? "Chargement…" : "Charger plus"}
</Text>
</Pressable>
)} */}
</View> </View>
)} )}
{filteredUsers.length === 0 && !usersLoading && ( {users.length === 0 && !usersLoading && (
<EmptyText text={"Aucun profil trouvé"} /> <EmptyText text={"Aucun profil trouvé"} />
)} )}
</CreateLyricsHeader> </CreateLyricsHeader>
</View> </View>
)} )}
{selected === "Playbacks" && (
<View style={{ paddingHorizontal: 2, paddingTop: 10 }}>
<CreateLyricsHeader
gradientProps={{ start: { x: 0, y: 0 }, end: { x: 0, y: 1 } }}
>
<EmptyText text={"Aucun playback trouvé"} />
</CreateLyricsHeader>
</View>
)}
{selected === "Clips" && ( {selected === "Clips" && (
<View style={{ paddingHorizontal: 2, paddingTop: 10 }}> <View style={{ paddingHorizontal: 2, paddingTop: 10 }}>
<CreateLyricsHeader <CreateLyricsHeader
-2
View File
@@ -87,7 +87,6 @@ const CreatingLyrics = ({ active, config, selections }) => {
await setIsLoading(true); await setIsLoading(true);
await updateProjectData({ await updateProjectData({
title: result?.title || "", title: result?.title || "",
titleLower: (result?.title || "").toLowerCase(),
lyrics: Array.isArray(result?.lyrics) ? result.lyrics : [], lyrics: Array.isArray(result?.lyrics) ? result.lyrics : [],
description: result?.lyricsDescription, description: result?.lyricsDescription,
config: config || null, config: config || null,
@@ -198,7 +197,6 @@ const CreatingLyrics = ({ active, config, selections }) => {
await setIsLoading(true); await setIsLoading(true);
await updateProjectData({ await updateProjectData({
title: result?.title || "", title: result?.title || "",
titleLower: (result?.title || "").toLowerCase(),
lyrics: Array.isArray(result?.lyrics) lyrics: Array.isArray(result?.lyrics)
? result.lyrics ? result.lyrics
: [], : [],
+6 -7
View File
@@ -112,7 +112,7 @@ const Lyrics = ({ navigation }) => {
if (invalid) { if (invalid) {
Alert.alert( Alert.alert(
"Champs incomplets", "Champs incomplets",
"Chaque couplet et refrain doit contenir du texte." "Chaque couplet et refrain doit contenir du texte.",
); );
return; return;
} }
@@ -133,7 +133,7 @@ const Lyrics = ({ navigation }) => {
normalizedOldLyrics.every( normalizedOldLyrics.every(
(s, i) => (s, i) =>
s.type === normalizedNewLyrics[i]?.type && s.type === normalizedNewLyrics[i]?.type &&
s.lyrics === normalizedNewLyrics[i]?.lyrics s.lyrics === normalizedNewLyrics[i]?.lyrics,
); );
// 1) Appel de la Cloud Function de modération avant tout enregistrement // 1) Appel de la Cloud Function de modération avant tout enregistrement
@@ -157,7 +157,7 @@ const Lyrics = ({ navigation }) => {
: null; : null;
Alert.alert( Alert.alert(
"Contenu interdit", "Contenu interdit",
[data?.message, quotes].filter(Boolean).join("\n\n") [data?.message, quotes].filter(Boolean).join("\n\n"),
); );
return; // stop here return; // stop here
} }
@@ -165,7 +165,7 @@ const Lyrics = ({ navigation }) => {
if (data?.errorCode === "ANALYSE_FAILED") { if (data?.errorCode === "ANALYSE_FAILED") {
Alert.alert( Alert.alert(
"Analyse indisponible", "Analyse indisponible",
"Impossible de vérifier la toxicité pour le moment. Réessayez plus tard." "Impossible de vérifier la toxicité pour le moment. Réessayez plus tard.",
); );
return; return;
} }
@@ -200,18 +200,17 @@ const Lyrics = ({ navigation }) => {
} catch (moderationError) { } catch (moderationError) {
console.log( console.log(
"Moderation call failed", "Moderation call failed",
moderationError?.message || moderationError moderationError?.message || moderationError,
); );
Alert.alert( Alert.alert(
"Analyse indisponible", "Analyse indisponible",
"Impossible de vérifier la toxicité pour le moment. Réessayez plus tard." "Impossible de vérifier la toxicité pour le moment. Réessayez plus tard.",
); );
return; return;
} }
const baseData = { const baseData = {
title: titleTrimmed, title: titleTrimmed,
titleLower: titleTrimmed.toLowerCase(),
lyrics: normalizedNewLyrics, lyrics: normalizedNewLyrics,
config: sanitize(projectConfig), config: sanitize(projectConfig),
selections: sanitize(projectSelections), selections: sanitize(projectSelections),