continue generating flow, add backend connection on main tabs
This commit is contained in:
@@ -13,9 +13,10 @@ const BorderGradientButton = ({
|
||||
titleStyle,
|
||||
containerStyle = {},
|
||||
tint = "dark",
|
||||
disabled = false,
|
||||
}) => {
|
||||
return (
|
||||
<Pressable onPress={onPress} style={{ ...containerStyle }}>
|
||||
<Pressable onPress={onPress} disabled={disabled} style={{ ...containerStyle, opacity: disabled ? 0.6 : 1 }}>
|
||||
<BorderGradient
|
||||
gradientProps={{
|
||||
colors: ["#F94697", "#7023F7"],
|
||||
|
||||
+41
-11
@@ -3,6 +3,7 @@ import { View, Text, Image, Pressable, StyleSheet } from "react-native";
|
||||
import { BlurView } from "expo-blur";
|
||||
|
||||
import Switch from "../components/Switch";
|
||||
import OptionSelector from "../components/OptionSelector";
|
||||
import { Container, Title, Input, Button } from "../components/Dialog";
|
||||
|
||||
import { Fonts, Style, gutters } from "../styles";
|
||||
@@ -12,6 +13,15 @@ const labelOptions = {
|
||||
name: "Nouveau nom",
|
||||
email: "Nouvelle adresse email",
|
||||
password: "Nouveau mot de passe",
|
||||
language: "Nouvelle langue",
|
||||
};
|
||||
|
||||
const languageOptions = {
|
||||
fr: "Français",
|
||||
en: "English",
|
||||
es: "Español",
|
||||
de: "Deutsch",
|
||||
it: "Italiano",
|
||||
};
|
||||
|
||||
export default ({ itemKey, type, value, title, onUpdateValue }) => {
|
||||
@@ -51,7 +61,11 @@ export default ({ itemKey, type, value, title, onUpdateValue }) => {
|
||||
},
|
||||
})}
|
||||
>
|
||||
{itemKey === "password" ? "********" : value}
|
||||
{itemKey === "password"
|
||||
? "********"
|
||||
: itemKey === "language"
|
||||
? languageOptions[value] || value
|
||||
: value}
|
||||
</Text>
|
||||
|
||||
<Image
|
||||
@@ -84,20 +98,36 @@ export default ({ itemKey, type, value, title, onUpdateValue }) => {
|
||||
value={currentPassword}
|
||||
onChangeText={(text) => setCurrentPassword(text)}
|
||||
keyboardType="visible-password"
|
||||
type={"password"}
|
||||
containerStyle={{ marginBottom: gutters / 2 }}
|
||||
/>
|
||||
)}
|
||||
{itemKey === "language" ? (
|
||||
<OptionSelector
|
||||
optionTypeList={languageOptions}
|
||||
selected={inputData || "fr"}
|
||||
setSelected={setInputData}
|
||||
containerStyle={{ marginBottom: gutters / 2 }}
|
||||
colorMap={{
|
||||
fr: { primary: "#F94697", secondary: "#F946971A" },
|
||||
en: { primary: "#7023F7", secondary: "#7023F71A" },
|
||||
es: { primary: "#FDBA74", secondary: "#FDBA741A" },
|
||||
de: { primary: "#60A5FA", secondary: "#60A5FA1A" },
|
||||
it: { primary: "#34D399", secondary: "#34D3991A" },
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
label={labelOptions[itemKey]}
|
||||
value={inputData}
|
||||
onChangeText={(text) => setInputData(text)}
|
||||
autoCapitalize={
|
||||
["password", "email"].includes(itemKey) ? "none" : "words"
|
||||
}
|
||||
type={itemKey}
|
||||
containerStyle={{ marginBottom: gutters / 2 }}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
label={labelOptions[itemKey]}
|
||||
value={inputData}
|
||||
onChangeText={(text) => setInputData(text)}
|
||||
autoCapitalize={
|
||||
["password", "email"].includes(itemKey) ? "none" : "words"
|
||||
}
|
||||
type={itemKey}
|
||||
containerStyle={{ marginBottom: gutters / 2 }}
|
||||
/>
|
||||
<Button
|
||||
label="Valider"
|
||||
onPress={() => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { FONT_FAMILY } from "../styles/Fonts";
|
||||
|
||||
const INITIAL_BOX_SIZE = 6;
|
||||
|
||||
export default ({ value, maxValue, progress, onSeek, seekEnabled = false }) => {
|
||||
export default ({ value, maxValue, progress, onSeek, onSeekStart, onSeekEnd, seekEnabled = false }) => {
|
||||
const offset = useSharedValue(0);
|
||||
const boxWidth = useSharedValue(INITIAL_BOX_SIZE);
|
||||
const [layout, setLayout] = useState(null);
|
||||
@@ -22,6 +22,12 @@ export default ({ value, maxValue, progress, onSeek, seekEnabled = false }) => {
|
||||
|
||||
const pan = Gesture.Pan()
|
||||
.enabled(seekEnabled)
|
||||
.onBegin(() => {
|
||||
if (seekEnabled && typeof onSeekStart === 'function') {
|
||||
// Notify JS thread that user started seeking (e.g., pause audio)
|
||||
runOnJS(onSeekStart)();
|
||||
}
|
||||
})
|
||||
.onChange((event) => {
|
||||
offset.value =
|
||||
Math.abs(offset.value) <= MAX_VALUE
|
||||
@@ -40,6 +46,11 @@ export default ({ value, maxValue, progress, onSeek, seekEnabled = false }) => {
|
||||
const ratio = MAX_VALUE > 0 ? Math.min(1, Math.max(0, offset.value / MAX_VALUE)) : 0;
|
||||
// Reanimated -> JS thread bridge
|
||||
runOnJS(onSeek)(ratio);
|
||||
})
|
||||
.onFinalize(() => {
|
||||
if (seekEnabled && typeof onSeekEnd === 'function') {
|
||||
runOnJS(onSeekEnd)();
|
||||
}
|
||||
});
|
||||
|
||||
// Reflect external progress into the slider UI
|
||||
|
||||
@@ -1,17 +1,68 @@
|
||||
import { View, Text } from "react-native";
|
||||
import React from "react";
|
||||
import React, { useGlobal } from "reactn";
|
||||
import AppActionSheet from "../AppActionSheet";
|
||||
import BorderGradientButton from "../BorderGradientButton";
|
||||
import GradientButton from "../GradientButton";
|
||||
import { Palette } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import alert from "../Alert";
|
||||
import firebase, { usersRef } from "../../config/firebase";
|
||||
import { reset } from "../../navigation/NavigationService";
|
||||
import { Routes } from "../../navigation";
|
||||
|
||||
const DeleteAccountModal = () => {
|
||||
const [currentUID] = useGlobal("currentUID");
|
||||
const [, setTooltip] = useGlobal("_tooltip");
|
||||
|
||||
const onClose = async () => {
|
||||
await SheetManager.hide("DeleteAccount");
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
alert(
|
||||
"Êtes-vous sûr ?",
|
||||
"Cette action supprimera votre profil.",
|
||||
[
|
||||
{ text: "Annuler", style: "cancel", onPress: () => {} },
|
||||
{
|
||||
text: "Confirmer",
|
||||
onPress: () => {
|
||||
alert(
|
||||
"Confirmation",
|
||||
"Dernière vérification: supprimer définitivement votre profil ?",
|
||||
[
|
||||
{ text: "Non", style: "cancel", onPress: () => {} },
|
||||
{
|
||||
text: "Oui, supprimer",
|
||||
onPress: async () => {
|
||||
try {
|
||||
if (!currentUID) return;
|
||||
await usersRef.doc(currentUID).delete();
|
||||
// Sign out and reset navigation to Login
|
||||
await firebase.auth().signOut();
|
||||
reset({ index: 0, routes: [{ name: Routes.Login }] });
|
||||
setTooltip({ type: "success", text: "Profil supprimé" });
|
||||
} catch (e) {
|
||||
setTooltip({
|
||||
type: "error",
|
||||
text: e?.message || "Suppression impossible",
|
||||
});
|
||||
} finally {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
{ cancelable: true },
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
{ cancelable: true },
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<AppActionSheet id="DeleteAccount">
|
||||
<View style={{ gap: 20 }}>
|
||||
@@ -39,7 +90,7 @@ const DeleteAccountModal = () => {
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ width: "80%", alignSelf: "center", gap: 12 }}>
|
||||
<BorderGradientButton title="Oui, supprimer" onPress={onClose} />
|
||||
<BorderGradientButton title="Oui, supprimer" onPress={confirmDelete} />
|
||||
<GradientButton title="Non, annuler" onPress={onClose} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -33,9 +33,21 @@ if (!firebase?.apps?.filter(({ name_ }) => name_ === "[DEFAULT]").length) {
|
||||
console.log("Firebase init");
|
||||
}
|
||||
|
||||
// Firestore settings for React Native/iOS: avoid streaming transport issues
|
||||
const firestore = firebase.firestore();
|
||||
try {
|
||||
// Prefer auto-detect; disable fetch streams for RN iOS stability
|
||||
firebase.firestore().settings({
|
||||
experimentalAutoDetectLongPolling: true,
|
||||
useFetchStreams: false,
|
||||
ignoreUndefinedProperties: true,
|
||||
});
|
||||
} catch (e) {
|
||||
// Ignore if settings were already set elsewhere
|
||||
}
|
||||
|
||||
export const usersRef = firestore.collection("users");
|
||||
export const projectsRef = firestore.collection("projects");
|
||||
export const notificationsRef = firestore.collection("notifications");
|
||||
export const documentsRef = firestore.collection("documents");
|
||||
export const chatsRef = firestore.collection("chats");
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useGlobal } from "reactn";
|
||||
import firebase from "../config/firebase";
|
||||
import useDataFromRef from "./useDataFromRef";
|
||||
|
||||
export default function useUserLikedProjects() {
|
||||
const [currentUID] = useGlobal("currentUID");
|
||||
|
||||
const { data, loading } = useDataFromRef({
|
||||
ref:
|
||||
currentUID
|
||||
? firebase
|
||||
.firestore()
|
||||
.collection("projects")
|
||||
.where("likedBy", "array-contains", currentUID)
|
||||
: null,
|
||||
simpleRef: false,
|
||||
listener: true,
|
||||
condition: !!currentUID,
|
||||
refreshArray: [currentUID],
|
||||
});
|
||||
|
||||
return { projects: Array.isArray(data) ? data : [], loading };
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ import Language from "../screens/Profile/Language";
|
||||
import Reels from "../screens/Profile/Reels";
|
||||
import Register from "../screens/Register";
|
||||
import CreatePassword from "../screens/CreatePassword";
|
||||
import CreateName from "../screens/CreateName";
|
||||
import CreatePseudo from "../screens/CreatePseudo";
|
||||
|
||||
const screenOptions = {
|
||||
headerShown: false,
|
||||
@@ -279,8 +279,8 @@ const screens = [
|
||||
component: CreatePassword,
|
||||
},
|
||||
{
|
||||
name: Routes.CreateName,
|
||||
component: CreateName,
|
||||
name: Routes.CreatePseudo,
|
||||
component: CreatePseudo,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ export const Routes = {
|
||||
ForgotPassword: "ForgotPassword",
|
||||
Register: "Register",
|
||||
CreatePassword: "CreatePassword",
|
||||
CreateName: "CreateName",
|
||||
CreatePseudo: "CreatePseudo",
|
||||
|
||||
BottomTab: "BottomTab",
|
||||
Welcome: "Welcome",
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useGlobal,
|
||||
setGlobal,
|
||||
} from "reactn";
|
||||
import messaging from "@react-native-firebase/messaging";
|
||||
import notifee, { EventType } from "@notifee/react-native";
|
||||
import { CommonActions } from "@react-navigation/core";
|
||||
import { useAppState } from "@react-native-community/hooks";
|
||||
import { Linking } from "react-native";
|
||||
|
||||
import { dispatch } from "../navigation/NavigationService";
|
||||
import { Routes } from "../navigation";
|
||||
import firebase, { usersRef } from "../config/firebase";
|
||||
import alert from "../components/Alert";
|
||||
|
||||
export const NotificationContext = createContext();
|
||||
|
||||
export async function displayNotification(data) {
|
||||
const { title, message, picture = null, video = null } = data || {};
|
||||
|
||||
let ios = {
|
||||
categoryId: "default",
|
||||
};
|
||||
|
||||
if (picture) {
|
||||
ios.attachments = [{ url: picture }];
|
||||
}
|
||||
|
||||
if (video) {
|
||||
ios.attachments = [
|
||||
{
|
||||
url: video,
|
||||
thumbnailTime: data?.thumbnailTime
|
||||
? parseInt(data?.thumbnailTime, 10)
|
||||
: 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
await notifee.displayNotification({
|
||||
title,
|
||||
body: message,
|
||||
ios,
|
||||
android: {
|
||||
channelId: "default",
|
||||
},
|
||||
data,
|
||||
});
|
||||
|
||||
await notifee.incrementBadgeCount();
|
||||
}
|
||||
|
||||
export async function triggerEvent({ type, detail }) {
|
||||
const { notification } = detail;
|
||||
|
||||
if (type === EventType.PRESS) {
|
||||
const { data = {} } = notification || {};
|
||||
const { projectID = null, taskID = null, messageID = null } = data || {};
|
||||
|
||||
if (projectID) {
|
||||
setGlobal({ currentProjectID: projectID });
|
||||
|
||||
if (taskID) {
|
||||
dispatch(
|
||||
CommonActions.navigate({
|
||||
name: Routes.TaskDetails,
|
||||
params: { taskID, messageID },
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ({ children }) => {
|
||||
const [currentUID] = useGlobal("currentUID");
|
||||
|
||||
const currentAppState = useAppState();
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUID) {
|
||||
updateFCMToken({ uid: currentUID });
|
||||
}
|
||||
}, [currentUID]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = messaging().onMessage(async (remoteMessage) => {
|
||||
console.log("Message handled in the foreground!", remoteMessage);
|
||||
const { data } = remoteMessage;
|
||||
|
||||
await displayNotification(data);
|
||||
});
|
||||
const foregroundSubscription = notifee.onForegroundEvent(triggerEvent);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
foregroundSubscription();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentAppState === "active") {
|
||||
notifee.setBadgeCount(0);
|
||||
}
|
||||
}, [currentAppState]);
|
||||
|
||||
const updateFCMToken = async ({ uid }) => {
|
||||
try {
|
||||
const authResponse = await messaging().requestPermission();
|
||||
const enabled = authResponse === messaging.AuthorizationStatus.AUTHORIZED;
|
||||
if (enabled) {
|
||||
const fcmToken = await messaging().getToken();
|
||||
if (fcmToken) {
|
||||
await usersRef.doc(uid).update({
|
||||
fcmTokens: firebase.firestore.FieldValue.arrayUnion(fcmToken),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
alert(
|
||||
"Activez les notifications",
|
||||
"Afin de rester informé de l'avancée de votre projet, il est important d'activer les notifications.",
|
||||
[
|
||||
{
|
||||
text: "Annuler",
|
||||
onPress: () => {},
|
||||
style: "cancel",
|
||||
},
|
||||
{
|
||||
text: "Paramètres",
|
||||
onPress: async () => {
|
||||
Linking.openSettings();
|
||||
},
|
||||
style: "confirm",
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<NotificationContext.Provider value={{}}>
|
||||
{children}
|
||||
</NotificationContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useNotification = () => {
|
||||
return useContext(NotificationContext);
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
import React, { useContext, createContext, useEffect, useState } from "react";
|
||||
|
||||
import firebase from "../config/firebase";
|
||||
import { useUserData } from "./UserDataProvider";
|
||||
|
||||
export const PremiumContext = createContext();
|
||||
|
||||
export default ({ children }) => {
|
||||
const { currentUID } = useUserData();
|
||||
|
||||
const [subscriptions, setSubscriptions] = useState(null);
|
||||
|
||||
const subscriptionType = subscriptions?.length > 0 ? "PREMIUM" : "FREE";
|
||||
const isPaying = subscriptionType !== "FREE";
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUID) {
|
||||
getPremiumStatus();
|
||||
}
|
||||
}, [currentUID]);
|
||||
|
||||
const getPremiumStatus = async () => {
|
||||
try {
|
||||
const { data } = await firebase
|
||||
.functions()
|
||||
.httpsCallable("payment-getPremiumStatus")({
|
||||
userID: currentUID,
|
||||
});
|
||||
|
||||
if (data?.subscriptions?.length) {
|
||||
setSubscriptions(
|
||||
(data.subscriptions || [])?.filter((sub) => sub.status === "active")
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PremiumContext.Provider
|
||||
value={{
|
||||
getPremiumStatus,
|
||||
|
||||
subscriptionType,
|
||||
subscriptions,
|
||||
|
||||
isPaying,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PremiumContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const usePremium = () => useContext(PremiumContext);
|
||||
@@ -6,7 +6,6 @@ import UserDataProvider from "./UserDataProvider";
|
||||
import WebViewProvider from "./WebViewProvider";
|
||||
import SplashAnimationProvider from "./SplashAnimationProvider";
|
||||
import UniversalLinkProvider from "./UniversalLinkProvider";
|
||||
import PremiumProvider from "./PremiumProvider";
|
||||
import StripeEmbeddedProvider from "./StripeEmbeddedProvider";
|
||||
import { SheetProvider } from "react-native-actions-sheet";
|
||||
|
||||
@@ -18,7 +17,6 @@ const SharedProviders = ({ children }) => {
|
||||
[WebViewProvider, {}],
|
||||
[UserDataProvider, {}],
|
||||
[StripeEmbeddedProvider, {}],
|
||||
[PremiumProvider, {}],
|
||||
[UniversalLinkProvider, {}],
|
||||
[SheetProvider, {}],
|
||||
];
|
||||
|
||||
@@ -18,6 +18,8 @@ export default ({ children }) => {
|
||||
const [currentUID] = useGlobal("currentUID");
|
||||
const [currentUserData, setCurrentUserData] = useGlobal("currentUserData");
|
||||
const [currentUserRoles] = useGlobal("currentUserRoles");
|
||||
const [userProjects, setUserProjects] = useState([]);
|
||||
const [userLikedProjects, setUserLikedProjects] = useState([]);
|
||||
|
||||
useDataFromRef({
|
||||
ref: currentUID ? usersRef.doc(currentUID) : null,
|
||||
@@ -34,6 +36,38 @@ export default ({ children }) => {
|
||||
},
|
||||
});
|
||||
|
||||
// Subscribe to user's projects (musics)
|
||||
useDataFromRef({
|
||||
ref: currentUID
|
||||
? firebase
|
||||
.firestore()
|
||||
.collection("projects")
|
||||
.where("userId", "==", currentUID)
|
||||
.orderBy("updatedAt", "desc")
|
||||
: null,
|
||||
simpleRef: false,
|
||||
listener: true,
|
||||
condition: !!currentUID,
|
||||
refreshArray: [currentUID],
|
||||
onUpdate: (list) => setUserProjects(Array.isArray(list) ? list : []),
|
||||
});
|
||||
|
||||
// Subscribe to user's liked projects
|
||||
useDataFromRef({
|
||||
ref: currentUID
|
||||
? firebase
|
||||
.firestore()
|
||||
.collection("projects")
|
||||
.where("likedBy", "array-contains", currentUID)
|
||||
.orderBy("updatedAt", "desc")
|
||||
: null,
|
||||
simpleRef: false,
|
||||
listener: true,
|
||||
condition: !!currentUID,
|
||||
refreshArray: [currentUID],
|
||||
onUpdate: (list) => setUserLikedProjects(Array.isArray(list) ? list : []),
|
||||
});
|
||||
|
||||
const updatePendingUserData = (newData) => {
|
||||
setPendingUserData((prevData) => ({
|
||||
...prevData,
|
||||
@@ -144,6 +178,8 @@ export default ({ children }) => {
|
||||
currentUserRoles,
|
||||
currentUID,
|
||||
currentUserData,
|
||||
userProjects,
|
||||
userLikedProjects,
|
||||
|
||||
setCurrentUserData,
|
||||
|
||||
@@ -163,3 +199,8 @@ export default ({ children }) => {
|
||||
export const useUserData = () => {
|
||||
return useContext(UserDataContext);
|
||||
};
|
||||
|
||||
// Convenience alias returning the same context, as requested
|
||||
export const useUser = () => {
|
||||
return useContext(UserDataContext);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from "react";
|
||||
import { MinuitProvider } from "react-native-minuit";
|
||||
|
||||
import SharedProviders from "./SharedProviders";
|
||||
import NotificationProvider from "./NotificationProvider";
|
||||
|
||||
import { Palette } from "../styles";
|
||||
|
||||
@@ -16,9 +15,7 @@ export default ({ children }) => {
|
||||
destructive: "red",
|
||||
}}
|
||||
>
|
||||
<SharedProviders>
|
||||
<NotificationProvider>{children}</NotificationProvider>
|
||||
</SharedProviders>
|
||||
<SharedProviders>{children}</SharedProviders>
|
||||
</MinuitProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -45,6 +45,18 @@ export default ({ navigation }) => {
|
||||
value: "",
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
title: "Notifications",
|
||||
key: "notifications",
|
||||
value: !!currentUserData?.notifications,
|
||||
type: "boolean",
|
||||
},
|
||||
{
|
||||
title: "Langue",
|
||||
key: "language",
|
||||
value: currentUserData?.language || "",
|
||||
type: "string",
|
||||
},
|
||||
];
|
||||
|
||||
const destructiveSettingsList = [
|
||||
@@ -125,7 +137,11 @@ export default ({ navigation }) => {
|
||||
}
|
||||
};
|
||||
|
||||
if (!value) {
|
||||
// Validate input except for booleans where false is allowed
|
||||
const isInvalidString =
|
||||
typeof value === "string" && value.trim().length === 0;
|
||||
const isNullish = value === null || value === undefined;
|
||||
if (isNullish || isInvalidString) {
|
||||
throw new Error("Vérifiez les informations saisies");
|
||||
}
|
||||
|
||||
@@ -269,6 +285,12 @@ export default ({ navigation }) => {
|
||||
);
|
||||
})}
|
||||
|
||||
<ItemRowList
|
||||
title={"Gérer mon abonnement"}
|
||||
action={() => {}}
|
||||
containerStyle={{}}
|
||||
/>
|
||||
|
||||
{destructiveSettingsList.map((setting, index) => (
|
||||
<ItemRowList key={index} {...setting} containerStyle={{}} />
|
||||
))}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { View, Text } from "react-native";
|
||||
import React from "react";
|
||||
import Page from "../layouts/Page";
|
||||
import { background } from "../assets";
|
||||
import ItemContainer from "../components/ItemContainer/ItemContainer";
|
||||
import { Palette } from "../styles";
|
||||
import { FONT_FAMILY } from "../styles/Fonts";
|
||||
import { Input } from "../components/Input";
|
||||
import GradientButton from "../components/GradientButton";
|
||||
import { navigate } from "../navigation/NavigationService";
|
||||
import { Routes } from "../navigation";
|
||||
|
||||
const CreateName = () => {
|
||||
return (
|
||||
<Page
|
||||
backgroundImg={background.homeBG}
|
||||
headerType="NAVIGATION"
|
||||
title="Inscription"
|
||||
>
|
||||
<View style={{ flex: 1, paddingTop: 20 }}>
|
||||
<ItemContainer height={275}>
|
||||
<View style={{ gap: 32, paddingTop: 5, paddingHorizontal: 5 }}>
|
||||
<View style={{ gap: 16 }}>
|
||||
<View style={{ gap: 2 }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 22,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
}}
|
||||
>
|
||||
Comment t’appelles-tu?
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: Palette.gray,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
Invisible pour les autres, sauf si tu l’utilises comme nom
|
||||
d’artiste.
|
||||
</Text>
|
||||
</View>
|
||||
<Input placeholder="Prénom" label="Prénom" isBlur />
|
||||
</View>
|
||||
<GradientButton
|
||||
title="Suivant"
|
||||
containerStyle={{
|
||||
width: "80%",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
onPress={() => navigate(Routes.Login)}
|
||||
/>
|
||||
</View>
|
||||
</ItemContainer>
|
||||
</View>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateName;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { View, Text, Pressable } from "react-native";
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useGlobal } from "reactn";
|
||||
import Page from "../layouts/Page";
|
||||
import { background } from "../assets";
|
||||
import ItemContainer from "../components/ItemContainer/ItemContainer";
|
||||
@@ -8,9 +9,43 @@ import { FONT_FAMILY } from "../styles/Fonts";
|
||||
import { Input } from "../components/Input";
|
||||
import GradientButton from "../components/GradientButton";
|
||||
import { navigate } from "../navigation/NavigationService";
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
import firebase, { usersRef } from "../config/firebase";
|
||||
import { Routes } from "../navigation";
|
||||
|
||||
const CreatePassword = () => {
|
||||
const route = useRoute();
|
||||
const email = route?.params?.email || "";
|
||||
const [password, setPassword] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [, setTooltip] = useGlobal("_tooltip");
|
||||
|
||||
const onCreateAccount = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const cred = await firebase
|
||||
.auth()
|
||||
.createUserWithEmailAndPassword(email.trim(), password);
|
||||
const uid = cred?.user?.uid || firebase.auth().currentUser?.uid;
|
||||
if (!uid) throw new Error("Création de compte échouée");
|
||||
await usersRef.doc(uid).set(
|
||||
{
|
||||
email: email.trim(),
|
||||
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
setTooltip({ text: "Compte créé, continuons", type: "success" });
|
||||
navigate(Routes.CreatePseudo);
|
||||
} catch (e) {
|
||||
console.log("Register error", e?.message);
|
||||
setTooltip({ text: e?.message || "Création impossible", type: "error" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Page
|
||||
backgroundImg={background.homeBG}
|
||||
@@ -46,15 +81,18 @@ const CreatePassword = () => {
|
||||
label="Mot de passe"
|
||||
isBlur
|
||||
type="password"
|
||||
value={password}
|
||||
setValue={setPassword}
|
||||
/>
|
||||
</View>
|
||||
<GradientButton
|
||||
title="Suivant"
|
||||
title={loading ? "Création..." : "Suivant"}
|
||||
containerStyle={{
|
||||
width: "80%",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
onPress={() => navigate(Routes.CreateName)}
|
||||
onPress={onCreateAccount}
|
||||
disabled={loading || !email || !password}
|
||||
/>
|
||||
</View>
|
||||
</ItemContainer>
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { View, Text } from "react-native";
|
||||
import React, { useState } from "react";
|
||||
import { useGlobal } from "reactn";
|
||||
import Page from "../layouts/Page";
|
||||
import { background } from "../assets";
|
||||
import ItemContainer from "../components/ItemContainer/ItemContainer";
|
||||
import { Palette } from "../styles";
|
||||
import { FONT_FAMILY } from "../styles/Fonts";
|
||||
import { Input } from "../components/Input";
|
||||
import GradientButton from "../components/GradientButton";
|
||||
import { navigate } from "../navigation/NavigationService";
|
||||
import { Routes } from "../navigation";
|
||||
import firebase, { usersRef } from "../config/firebase";
|
||||
|
||||
const CreatePseudo = () => {
|
||||
const [userName, setUserName] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [, setTooltip] = useGlobal("_tooltip");
|
||||
|
||||
const onCreate = async () => {
|
||||
const raw = userName || "";
|
||||
const val = raw.trim();
|
||||
if (!val) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
const uid = firebase.auth().currentUser?.uid;
|
||||
if (!uid) {
|
||||
return navigate(Routes.Login);
|
||||
}
|
||||
// Check uniqueness (case-insensitive)
|
||||
const lower = val.toLowerCase();
|
||||
const snap = await usersRef.where("userNameLower", "==", lower).limit(1).get();
|
||||
if (!snap.empty && snap.docs[0].id !== uid) {
|
||||
setTooltip({ text: "Ce pseudo est déjà pris", type: "error" });
|
||||
return;
|
||||
}
|
||||
|
||||
await usersRef.doc(uid).set(
|
||||
{
|
||||
userName: val,
|
||||
userNameLower: lower,
|
||||
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
setTooltip({ text: `Bienvenue ${val}`, type: "success" });
|
||||
navigate(Routes.BottomTab);
|
||||
} catch (e) {
|
||||
console.log("CreatePseudo error", e?.message);
|
||||
setTooltip({ text: e?.message || "Enregistrement impossible", type: "error" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Page
|
||||
backgroundImg={background.homeBG}
|
||||
headerType="NAVIGATION"
|
||||
title="Ton pseudo"
|
||||
hideBackButton
|
||||
>
|
||||
<View style={{ flex: 1, paddingTop: 20 }}>
|
||||
<ItemContainer height={275}>
|
||||
<View style={{ gap: 32, paddingTop: 5, paddingHorizontal: 5 }}>
|
||||
<View style={{ gap: 16 }}>
|
||||
<View style={{ gap: 2 }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 22,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
}}
|
||||
>
|
||||
Choisis ton pseudo
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: Palette.gray,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
Visible par les autres utilisateurs.
|
||||
</Text>
|
||||
</View>
|
||||
<Input
|
||||
placeholder="Pseudo"
|
||||
label="Pseudo"
|
||||
isBlur
|
||||
value={userName}
|
||||
setValue={setUserName}
|
||||
/>
|
||||
</View>
|
||||
<GradientButton
|
||||
title={loading ? "Chargement..." : "Valider"}
|
||||
containerStyle={{
|
||||
width: "80%",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
onPress={onCreate}
|
||||
disabled={loading || !userName?.trim()}
|
||||
/>
|
||||
</View>
|
||||
</ItemContainer>
|
||||
</View>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreatePseudo;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { View, Text, Image, Pressable, Platform } from "react-native";
|
||||
import React, { useState } from "react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import Page from "../layouts/Page";
|
||||
import { ai, background, icons } from "../assets";
|
||||
import { Palette, Style } from "../styles";
|
||||
@@ -9,10 +9,29 @@ import { BlurView } from "expo-blur";
|
||||
import { FONT_FAMILY } from "../styles/Fonts";
|
||||
import { size } from "../styles/Style";
|
||||
import GradientButton from "../components/GradientButton";
|
||||
import { goBack } from "../navigation/NavigationService";
|
||||
import { goBack, navigate } from "../navigation/NavigationService";
|
||||
import { Routes } from "../navigation";
|
||||
import { useUser } from "../providers/UserDataProvider";
|
||||
|
||||
const CreateSongs = () => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(null);
|
||||
const { userProjects = [] } = useUser();
|
||||
const projects = useMemo(
|
||||
() => (Array.isArray(userProjects) ? userProjects.slice(0, 3) : []),
|
||||
[userProjects],
|
||||
);
|
||||
const fmtDate = (ts) => {
|
||||
try {
|
||||
const d = ts?.toDate ? ts.toDate() : ts ? new Date(ts) : null;
|
||||
if (!d || Number.isNaN(d.getTime())) return "";
|
||||
const dd = `${d.getDate().toString().padStart(2, "0")}/${(d.getMonth() + 1)
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
return dd;
|
||||
} catch (_) {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Page backgroundImg={background.homeBG} headerType="NONE">
|
||||
@@ -102,7 +121,7 @@ const CreateSongs = () => {
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ gap: 10, zIndex: 1 }}>
|
||||
{Array.from({ length: 3 }).map((item, index) => (
|
||||
{projects.map((item, index) => (
|
||||
<Pressable
|
||||
key={index}
|
||||
onPress={() => {
|
||||
@@ -140,7 +159,7 @@ const CreateSongs = () => {
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
Chanson sans titre
|
||||
{item?.title || "Sans titre"}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
@@ -152,7 +171,7 @@ const CreateSongs = () => {
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
Modifié le 23/04
|
||||
{`Modifié le ${fmtDate(item?.updatedAt || item?.createdAt)}`}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable>
|
||||
@@ -175,12 +194,13 @@ const CreateSongs = () => {
|
||||
))}
|
||||
</View>
|
||||
<GradientButton
|
||||
title="Créer un texte"
|
||||
title={selectedIndex !== null ? "Poursuivre la création" : "Créer un texte"}
|
||||
containerStyle={{
|
||||
width: "80%",
|
||||
alignSelf: "center",
|
||||
marginTop: 10,
|
||||
}}
|
||||
onPress={() => navigate(Routes.Writing)}
|
||||
/>
|
||||
</BlurView>
|
||||
</BorderGradient>
|
||||
|
||||
@@ -9,9 +9,23 @@ import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
import SongCard from "./components/SongCard";
|
||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
import PlaybacksCard from "./components/PlaybacksCard";
|
||||
import firebase from "../../config/firebase";
|
||||
import useDataFromRef from "../../hooks/useDataFromRef";
|
||||
import { navigate } from "../../navigation/NavigationService";
|
||||
import { Routes } from "../../navigation";
|
||||
|
||||
const HitParade = () => {
|
||||
const [selected, setSelected] = useState("Chansons");
|
||||
const { data: latestSongs } = useDataFromRef({
|
||||
ref: firebase
|
||||
.firestore()
|
||||
.collection("projects")
|
||||
.orderBy("createdAt", "desc")
|
||||
.limit(10),
|
||||
simpleRef: false,
|
||||
listener: true,
|
||||
condition: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<Page backgroundImg={background.hitParadeBG} headerType="NONE">
|
||||
@@ -80,8 +94,17 @@ const HitParade = () => {
|
||||
gap: 10,
|
||||
paddingBottom: responsiveHeight(20),
|
||||
}}
|
||||
data={Array.from({ length: 5 })}
|
||||
renderItem={() => <SongCard />}
|
||||
data={Array.isArray(latestSongs) ? latestSongs : []}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={({ item, index }) => (
|
||||
<SongCard
|
||||
rank={index + 1}
|
||||
title={item?.title || "Sans titre"}
|
||||
artist={"MusicLand"}
|
||||
coverUrl={item?.coverUrl || null}
|
||||
onPress={() => navigate(Routes.MusicDetails, { projectId: item.id })}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{selected === "Playbacks" && (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { View, Text, Image, Platform } from "react-native";
|
||||
import { View, Text, Image, Platform, Pressable } from "react-native";
|
||||
import React from "react";
|
||||
import { img } from "../../../assets";
|
||||
import { BlurView } from "expo-blur";
|
||||
@@ -7,16 +7,17 @@ import { Palette } from "../../../styles";
|
||||
import { FONT_FAMILY } from "../../../styles/Fonts";
|
||||
import CoinBadge from "./CoinBadge";
|
||||
|
||||
const SongCard = () => {
|
||||
const SongCard = ({ rank = 1, title = "Sans titre", artist = "MusicLand", coverUrl = null, onPress = null }) => {
|
||||
return (
|
||||
<View
|
||||
<Pressable
|
||||
onPress={onPress || undefined}
|
||||
style={{
|
||||
...Style.containerRow,
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={img.placeholder2}
|
||||
source={coverUrl ? { uri: coverUrl } : img.placeholder2}
|
||||
style={{ ...size({ size: 60 }), borderRadius: 12 }}
|
||||
/>
|
||||
<BlurView
|
||||
@@ -43,7 +44,7 @@ const SongCard = () => {
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
}}
|
||||
>
|
||||
1
|
||||
{rank}
|
||||
</Text>
|
||||
<View>
|
||||
<Text
|
||||
@@ -53,7 +54,7 @@ const SongCard = () => {
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
}}
|
||||
>
|
||||
Alors on danse
|
||||
{title}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
@@ -62,13 +63,13 @@ const SongCard = () => {
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
Stromae
|
||||
{artist}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<CoinBadge />
|
||||
</BlurView>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+14
-5
@@ -67,11 +67,20 @@ const Home = () => {
|
||||
{CREATE_DATA.map((item, index) => (
|
||||
<Pressable
|
||||
key={index}
|
||||
onPress={() =>
|
||||
navigate(Routes.Create, {
|
||||
action: item.type,
|
||||
})
|
||||
}
|
||||
onPress={() => {
|
||||
switch (index) {
|
||||
case 0:
|
||||
navigate(Routes.Create, {
|
||||
action: item.type,
|
||||
});
|
||||
break;
|
||||
case 1:
|
||||
navigate(Routes.Studio, {
|
||||
action: item.type,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<BlurView
|
||||
tint="dark"
|
||||
|
||||
@@ -8,10 +8,14 @@ import Animated, { Easing, FadeIn, FadeOut } from "react-native-reanimated";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { navigate } from "../../navigation/NavigationService";
|
||||
import { Routes } from "../../navigation";
|
||||
import { useUser } from "../../providers/UserDataProvider";
|
||||
|
||||
const AllMyLikedMusic = () => {
|
||||
const [top, setTop] = useState(0);
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const { userLikedProjects: projects } = useUser();
|
||||
|
||||
return (
|
||||
<Page
|
||||
@@ -22,26 +26,33 @@ const AllMyLikedMusic = () => {
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={{ gap: 10 }}>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<MusicCard
|
||||
key={index}
|
||||
onPressMore={(posTop) => {
|
||||
setTop(posTop);
|
||||
if (showMenu) {
|
||||
if (posTop === top) {
|
||||
setShowMenu(false);
|
||||
{Array.isArray(projects) &&
|
||||
projects.map((item) => (
|
||||
<MusicCard
|
||||
key={item.id}
|
||||
title={item?.title || "Sans titre"}
|
||||
imageUri={item?.coverUrl || null}
|
||||
subtitle={"MusicLand"}
|
||||
projectId={item?.id}
|
||||
likedBy={item?.likedBy || []}
|
||||
onPress={() => navigate(Routes.MusicDetails, { projectId: item.id })}
|
||||
onPressMore={(posTop) => {
|
||||
setTop(posTop);
|
||||
if (showMenu) {
|
||||
if (posTop === top) {
|
||||
setShowMenu(false);
|
||||
} else {
|
||||
setShowMenu(false);
|
||||
setTimeout(() => {
|
||||
setShowMenu(true);
|
||||
}, 300);
|
||||
}
|
||||
} else {
|
||||
setShowMenu(false);
|
||||
setTimeout(() => {
|
||||
setShowMenu(true);
|
||||
}, 300);
|
||||
setShowMenu(true);
|
||||
}
|
||||
} else {
|
||||
setShowMenu(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{showMenu && (
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(300).easing(Easing.ease)}
|
||||
|
||||
@@ -6,10 +6,14 @@ import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||
import { gutters, Palette } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import useLayoutType from "../../hooks/useLayoutType";
|
||||
import { useUser } from "../../providers/UserDataProvider";
|
||||
import { navigate } from "../../navigation/NavigationService";
|
||||
import { Routes } from "../../navigation";
|
||||
|
||||
const AllMyMusic = () => {
|
||||
const [containerLayout, setContainerLayout] = useState(null);
|
||||
const { isWeb } = useLayoutType();
|
||||
const { userProjects: projects } = useUser();
|
||||
|
||||
return (
|
||||
<Page
|
||||
@@ -39,17 +43,23 @@ const AllMyMusic = () => {
|
||||
}}
|
||||
>
|
||||
<FlatList
|
||||
data={Array.from({ length: 9 })}
|
||||
data={Array.isArray(projects) ? projects : []}
|
||||
numColumns={3}
|
||||
columnWrapperStyle={{ gap: 10 }}
|
||||
contentContainerStyle={{ gap: 14 }}
|
||||
renderItem={() => (
|
||||
keyExtractor={(item) => item.id}
|
||||
// Projects are provided by provider with a live listener
|
||||
renderItem={({ item }) => (
|
||||
<Pressable
|
||||
style={{ flex: 1, gap: 4 }}
|
||||
onPress={() => console.log("PRESSED")}
|
||||
onPress={() =>
|
||||
navigate(Routes.MusicDetails, { projectId: item.id })
|
||||
}
|
||||
>
|
||||
<Image
|
||||
source={img.placeholder2}
|
||||
source={
|
||||
item?.coverUrl ? { uri: item.coverUrl } : img.placeholder2
|
||||
}
|
||||
style={{ width: "100%", height: 114, borderRadius: 8 }}
|
||||
/>
|
||||
<Text
|
||||
@@ -59,7 +69,7 @@ const AllMyMusic = () => {
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
Voiture
|
||||
{item?.title || "Sans titre"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
} from "react-native";
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import Page from "../../layouts/Page";
|
||||
import { background, icons, img } from "../../assets";
|
||||
import { Palette } from "../../styles";
|
||||
@@ -15,16 +15,169 @@ import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import Slider from "../../components/Slider";
|
||||
import { useRoute } from "@react-navigation/core";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import firebase, { usersRef, projectsRef, arrayUnion, arrayRemove } from "../../config/firebase";
|
||||
import { useGlobal } from "reactn";
|
||||
import useDataFromRef from "../../hooks/useDataFromRef";
|
||||
import { Audio } from "expo-av";
|
||||
|
||||
const MusicDetails = () => {
|
||||
const params = useRoute().params;
|
||||
const params = useRoute().params || {};
|
||||
const action = params?.action;
|
||||
const projectId = params?.projectId || null;
|
||||
const [fav, setFav] = useState(false);
|
||||
const [currentUID] = useGlobal("currentUID");
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 });
|
||||
const wasPlayingBeforeSeek = React.useRef(false);
|
||||
|
||||
const { data: project } = useDataFromRef({
|
||||
ref: projectId
|
||||
? firebase.firestore().collection("projects").doc(projectId)
|
||||
: null,
|
||||
simpleRef: true,
|
||||
listener: true,
|
||||
condition: !!projectId,
|
||||
});
|
||||
|
||||
const { data: owner } = useDataFromRef({
|
||||
ref: project?.userId ? usersRef.doc(project.userId) : null,
|
||||
simpleRef: true,
|
||||
listener: true,
|
||||
condition: !!project?.userId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (project && currentUID) {
|
||||
const liked = Array.isArray(project?.likedBy)
|
||||
? project.likedBy.includes(currentUID)
|
||||
: false;
|
||||
setFav(liked);
|
||||
}
|
||||
}, [project?.likedBy, currentUID]);
|
||||
|
||||
const title = project?.title || "Sans titre";
|
||||
const artist = owner?.userName || "MusicLand";
|
||||
const coverUrl = project?.coverUrl || null;
|
||||
const songUrl = useMemo(() => {
|
||||
if (project?.song?.url) return project.song.url;
|
||||
const arr = Array.isArray(project?.musicUrls) ? project.musicUrls : [];
|
||||
return arr[0] || null;
|
||||
}, [project]);
|
||||
|
||||
const soundRef = React.useRef(null);
|
||||
|
||||
// Load/unload audio with expo-av for reliable status updates on iOS/Android
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
const load = async () => {
|
||||
try {
|
||||
// Unload previous
|
||||
if (soundRef.current) {
|
||||
await soundRef.current.unloadAsync();
|
||||
soundRef.current.setOnPlaybackStatusUpdate(null);
|
||||
soundRef.current = null;
|
||||
}
|
||||
if (!songUrl) return;
|
||||
const { sound } = await Audio.Sound.createAsync(
|
||||
{ uri: songUrl },
|
||||
{ shouldPlay: false },
|
||||
(status) => {
|
||||
if (!isMounted) return;
|
||||
if (!status || !status.isLoaded) return;
|
||||
const pos = status.positionMillis || 0;
|
||||
const dur = status.durationMillis || 0;
|
||||
setProgressInfo({ pos, dur });
|
||||
setIsPlaying(!!status.isPlaying);
|
||||
},
|
||||
);
|
||||
soundRef.current = sound;
|
||||
} catch (e) {
|
||||
console.log("Audio load error", e?.message);
|
||||
}
|
||||
};
|
||||
load();
|
||||
return () => {
|
||||
isMounted = false;
|
||||
(async () => {
|
||||
try {
|
||||
if (soundRef.current) {
|
||||
await soundRef.current.unloadAsync();
|
||||
soundRef.current.setOnPlaybackStatusUpdate(null);
|
||||
soundRef.current = null;
|
||||
}
|
||||
} catch (_) {}
|
||||
})();
|
||||
};
|
||||
}, [songUrl]);
|
||||
|
||||
const fmt = (ms) => {
|
||||
const total = Math.max(0, Math.floor((ms || 0) / 1000));
|
||||
const m = Math.floor(total / 60)
|
||||
.toString()
|
||||
.padStart(1, "0");
|
||||
const s = (total % 60).toString().padStart(2, "0");
|
||||
return `${m}:${s}`;
|
||||
};
|
||||
|
||||
const togglePlay = async () => {
|
||||
const sound = soundRef.current;
|
||||
if (!sound || !songUrl) return;
|
||||
try {
|
||||
const status = await sound.getStatusAsync();
|
||||
if (status?.isLoaded && status.isPlaying) {
|
||||
await sound.pauseAsync();
|
||||
setIsPlaying(false);
|
||||
} else {
|
||||
await sound.playAsync();
|
||||
setIsPlaying(true);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("MusicDetails audio error", e?.message);
|
||||
}
|
||||
};
|
||||
|
||||
const onSeek = async (ratio) => {
|
||||
try {
|
||||
const dur = progressInfo.dur || 0;
|
||||
const pos = Math.floor(dur * ratio);
|
||||
const sound = soundRef.current;
|
||||
if (sound && dur > 0) {
|
||||
await sound.setPositionAsync(pos);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("MusicDetails seek error", e?.message);
|
||||
}
|
||||
};
|
||||
|
||||
const seekBy = async (deltaSeconds) => {
|
||||
try {
|
||||
const cur = Math.floor((progressInfo.pos || 0) / 1000);
|
||||
const next = Math.max(0, cur + deltaSeconds);
|
||||
const sound = soundRef.current;
|
||||
if (sound) await sound.setPositionAsync(next * 1000);
|
||||
} catch (e) {
|
||||
console.log("MusicDetails seekBy error", e?.message);
|
||||
}
|
||||
};
|
||||
const description = useMemo(() => {
|
||||
// Try to build a readable text from lyrics if present
|
||||
if (Array.isArray(project?.lyrics)) {
|
||||
return project.lyrics
|
||||
.map((s) => (s?.lyrics || "").trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 4)
|
||||
.join("\n\n");
|
||||
}
|
||||
const c = project?.lyrics?.couplet;
|
||||
const r = project?.lyrics?.refrain;
|
||||
const parts = [c, r].filter(Boolean);
|
||||
return parts.length ? parts.join("\n\n") : "";
|
||||
}, [project]);
|
||||
|
||||
return (
|
||||
<Page
|
||||
headerType="NAVIGATION"
|
||||
title={action === "userProfile" ? "Mon profil" : "Recherche"}
|
||||
title={action === "userProfile" ? "Mon profil" : "Détail musique"}
|
||||
backgroundImg={
|
||||
action === "userProfile" ? background.profileBG : background.libraryBG2
|
||||
}
|
||||
@@ -48,14 +201,32 @@ const MusicDetails = () => {
|
||||
// stickyHeaderIndices={[1]}
|
||||
>
|
||||
<View style={{ gap: 28 }}>
|
||||
<Image source={img.placeholder4} style={styles.img} />
|
||||
<Image
|
||||
source={coverUrl ? { uri: coverUrl } : img.placeholder4}
|
||||
style={styles.img}
|
||||
/>
|
||||
<View style={{ ...Style.containerSpaceBetween }}>
|
||||
<View>
|
||||
<Text style={styles.title}>Lust for Life</Text>
|
||||
<Text style={styles.name}>Lana del Rey</Text>
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
<Text style={styles.name}>{artist}</Text>
|
||||
</View>
|
||||
<View style={{ ...Style.containerRow, gap: 12 }}>
|
||||
<Pressable onPress={() => setFav(!fav)}>
|
||||
<Pressable
|
||||
onPress={async () => {
|
||||
if (!projectId || !currentUID) return;
|
||||
const next = !fav;
|
||||
setFav(next);
|
||||
try {
|
||||
await projectsRef.doc(projectId).update({
|
||||
likedBy: next
|
||||
? arrayUnion(currentUID)
|
||||
: arrayRemove(currentUID),
|
||||
});
|
||||
} catch (e) {
|
||||
setFav(!next);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={fav ? icons.heart : icons.heartOutline}
|
||||
style={{ ...size({ size: 24 }) }}
|
||||
@@ -73,47 +244,91 @@ const MusicDetails = () => {
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ paddingTop: 22 }}>
|
||||
<Slider value={"0:00"} maxValue={"2:00"} />
|
||||
<Slider
|
||||
value={fmt(progressInfo.pos)}
|
||||
maxValue={fmt(progressInfo.dur)}
|
||||
progress={
|
||||
progressInfo.dur ? (progressInfo.pos || 0) / progressInfo.dur : 0
|
||||
}
|
||||
seekEnabled={!!songUrl}
|
||||
onSeekStart={async () => {
|
||||
try {
|
||||
const sound = soundRef.current;
|
||||
const status = await sound?.getStatusAsync?.();
|
||||
wasPlayingBeforeSeek.current =
|
||||
!!status?.isLoaded && !!status?.isPlaying;
|
||||
if (status?.isLoaded && status?.isPlaying) {
|
||||
await sound.pauseAsync();
|
||||
setIsPlaying(false);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Pause on seek start error", e?.message);
|
||||
}
|
||||
}}
|
||||
onSeek={onSeek}
|
||||
onSeekEnd={async () => {
|
||||
try {
|
||||
const sound = soundRef.current;
|
||||
if (sound && wasPlayingBeforeSeek.current) {
|
||||
await sound.playAsync();
|
||||
setIsPlaying(true);
|
||||
}
|
||||
wasPlayingBeforeSeek.current = false;
|
||||
} catch (e) {
|
||||
console.log("Resume after seek error", e?.message);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<View style={{ ...Style.containerRow, gap: 24, alignSelf: "center" }}>
|
||||
<Pressable>
|
||||
<Image source={icons.forward} />
|
||||
</Pressable>
|
||||
<Pressable>
|
||||
<Image source={icons.pause} />
|
||||
</Pressable>
|
||||
<Pressable>
|
||||
{/* Previous (rewind 10s) */}
|
||||
<Pressable onPress={() => seekBy(-10)}>
|
||||
<Image
|
||||
source={icons.forward}
|
||||
style={{ transform: [{ rotate: "180deg" }] }}
|
||||
style={{
|
||||
...size({ size: 30 }),
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
{/* Play / Pause */}
|
||||
<Pressable
|
||||
style={{
|
||||
...size({ size: 40 }),
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
onPress={togglePlay}
|
||||
>
|
||||
<Image
|
||||
resizeMode={"contain"}
|
||||
source={isPlaying ? icons.pause : icons.play}
|
||||
style={size({ size: 34 })}
|
||||
/>
|
||||
</Pressable>
|
||||
{/* Next (forward 10s) */}
|
||||
<Pressable onPress={() => seekBy(10)}>
|
||||
<Image
|
||||
source={icons.forward}
|
||||
style={{
|
||||
...size({ size: 30 }),
|
||||
transform: [{ rotate: "180deg" }],
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ marginTop: 30, gap: 20 }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
Description chanson. Viverra enim risus enim enim placerat. Integer
|
||||
pulvinar tristique suscipit risus. Id hendrerit in odio phasellus
|
||||
interdum lectus amet
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
Allô{"\n"}Écoute maman est près de toi{"\n"}Il faut lui dire "maman,
|
||||
c'est quelqu'un pour toi"{"\n"}Ah, c'est le monsieur de la dernière
|
||||
fois{"\n"}Bon, je vais la chercher{"\n"}Je crois qu'elle est dans
|
||||
son bain{"\n"}Et je sais pas si elle va pouvoir venir
|
||||
</Text>
|
||||
</View>
|
||||
{description?.length > 0 && (
|
||||
<View style={{ marginTop: 30, gap: 20 }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { View, Text, Image, FlatList } from "react-native";
|
||||
import { View, Image, FlatList, Pressable } from "react-native";
|
||||
import React from "react";
|
||||
import CardContainer from "./CardContainer";
|
||||
import { Style } from "../../../styles";
|
||||
import { img } from "../../../assets";
|
||||
import { navigate } from "../../../navigation/NavigationService";
|
||||
import { Routes } from "../../../navigation";
|
||||
import { useUser } from "../../../providers/UserDataProvider";
|
||||
|
||||
const LikedMusic = () => {
|
||||
const { userLikedProjects = [] } = useUser();
|
||||
const items = Array.isArray(userLikedProjects)
|
||||
? userLikedProjects.slice(0, 6)
|
||||
: [];
|
||||
return (
|
||||
<CardContainer
|
||||
label="Musiques likées"
|
||||
@@ -14,17 +19,21 @@ const LikedMusic = () => {
|
||||
>
|
||||
<FlatList
|
||||
scrollEnabled={false}
|
||||
data={Array.from({ length: 6 })}
|
||||
data={items}
|
||||
numColumns={3}
|
||||
contentContainerStyle={{ gap: 10 }}
|
||||
columnWrapperStyle={{ gap: 6 }}
|
||||
renderItem={() => (
|
||||
<View style={{ flex: 1 }}>
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<Pressable
|
||||
style={{ flex: 1 }}
|
||||
onPress={() => navigate(Routes.MusicDetails, { projectId: item.id })}
|
||||
>
|
||||
<Image
|
||||
source={img.placeholder3}
|
||||
source={item?.coverUrl ? { uri: item.coverUrl } : img.placeholder3}
|
||||
style={{ width: "100%", height: 172, borderRadius: 10 }}
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
)}
|
||||
/>
|
||||
</CardContainer>
|
||||
|
||||
@@ -12,8 +12,11 @@ import { icons, img } from "../../../assets";
|
||||
import { size } from "../../../styles/Style";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { FONT_FAMILY } from "../../../styles/Fonts";
|
||||
import { useGlobal } from "reactn";
|
||||
import { projectsRef, arrayUnion, arrayRemove } from "../../../config/firebase";
|
||||
|
||||
const MusicCard = ({ onPress, onPressMore }) => {
|
||||
const MusicCard = ({ onPress, onPressMore, title = "Sans titre", subtitle = "MusicLand", imageUri = null, projectId = null, likedBy = [] }) => {
|
||||
const [currentUID] = useGlobal("currentUID");
|
||||
const [selected, setSelected] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [layout, setLayout] = useState(null);
|
||||
@@ -26,6 +29,26 @@ const MusicCard = ({ onPress, onPressMore }) => {
|
||||
}
|
||||
}, [layout]);
|
||||
|
||||
useEffect(() => {
|
||||
if (Array.isArray(likedBy) && currentUID) {
|
||||
setSelected(likedBy.includes(currentUID));
|
||||
}
|
||||
}, [JSON.stringify(likedBy), currentUID]);
|
||||
|
||||
const toggleLike = async () => {
|
||||
if (!projectId || !currentUID) return;
|
||||
const next = !selected;
|
||||
setSelected(next);
|
||||
try {
|
||||
await projectsRef.doc(projectId).update({
|
||||
likedBy: next ? arrayUnion(currentUID) : arrayRemove(currentUID),
|
||||
});
|
||||
} catch (e) {
|
||||
// rollback on failure
|
||||
setSelected(!next);
|
||||
}
|
||||
};
|
||||
|
||||
const onPressMenu = () => {
|
||||
onPressMore?.(menuPos);
|
||||
};
|
||||
@@ -41,7 +64,7 @@ const MusicCard = ({ onPress, onPressMore }) => {
|
||||
onLayout={(e) => setLayout(e.nativeEvent.layout)}
|
||||
>
|
||||
<Image
|
||||
source={img.placeholder2}
|
||||
source={imageUri ? { uri: imageUri } : img.placeholder2}
|
||||
style={{ ...size({ size: 60 }), borderRadius: 12 }}
|
||||
/>
|
||||
<View style={styles.blurContainer}>
|
||||
@@ -57,8 +80,8 @@ const MusicCard = ({ onPress, onPressMore }) => {
|
||||
}
|
||||
>
|
||||
<View>
|
||||
<Text style={styles.title}>Alors on danse</Text>
|
||||
<Text style={styles.subTitle}>Stromae</Text>
|
||||
<Text style={styles.title}>{title || "Sans titre"}</Text>
|
||||
<Text style={styles.subTitle}>{subtitle || "MusicLand"}</Text>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
@@ -66,7 +89,7 @@ const MusicCard = ({ onPress, onPressMore }) => {
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<Pressable onPress={() => setSelected(!selected)}>
|
||||
<Pressable onPress={toggleLike}>
|
||||
<Image
|
||||
source={selected ? icons.heart : icons.heartOutline}
|
||||
style={size({ size: 24 })}
|
||||
|
||||
@@ -4,17 +4,32 @@ import CardContainer from "./CardContainer";
|
||||
import MusicCard from "./MusicCard";
|
||||
import { navigate } from "../../../navigation/NavigationService";
|
||||
import { Routes } from "../../../navigation";
|
||||
import { useUser } from "../../../providers/UserDataProvider";
|
||||
|
||||
const MyMusic = () => {
|
||||
const { userProjects = [] } = useUser();
|
||||
const projects = Array.isArray(userProjects)
|
||||
? userProjects.slice(0, 3)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<CardContainer
|
||||
label="Mes musiques"
|
||||
onPress={() => navigate(Routes.AllMyMusic)}
|
||||
>
|
||||
<View style={{ gap: 10 }}>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<MusicCard key={index} />
|
||||
))}
|
||||
{Array.isArray(projects) &&
|
||||
projects.map((p) => (
|
||||
<MusicCard
|
||||
key={p.id}
|
||||
title={p?.title}
|
||||
subtitle={"MusicLand"}
|
||||
imageUri={p?.coverUrl || null}
|
||||
projectId={p?.id}
|
||||
likedBy={p?.likedBy || []}
|
||||
onPress={() => navigate(Routes.MusicDetails, { projectId: p.id })}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</CardContainer>
|
||||
);
|
||||
|
||||
+48
-24
@@ -1,23 +1,50 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import React from "reactn";
|
||||
import React, { useState } from "react";
|
||||
import { useGlobal } from "reactn";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
|
||||
import Page from "../layouts/Page.js";
|
||||
import { background } from "../assets/index.js";
|
||||
import { background } from "../assets";
|
||||
import { Input } from "../components/Input.js";
|
||||
import GradientButton from "../components/GradientButton.js";
|
||||
import Palette from "../styles/Palette.js";
|
||||
import { FONT_FAMILY } from "../styles/Fonts.js";
|
||||
import { navigate } from "../navigation/NavigationService.js";
|
||||
import { Routes } from "../navigation/Routes.js";
|
||||
import { Routes } from "../navigation";
|
||||
import ItemContainer from "../components/ItemContainer/ItemContainer.js";
|
||||
import firebase, { usersRef } from "../config/firebase";
|
||||
|
||||
export default ({ navigation }) => {
|
||||
const [, setTooltip] = useGlobal("_tooltip");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onLogin = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await firebase.auth().signInWithEmailAndPassword(email.trim(), password);
|
||||
const uid = firebase.auth().currentUser?.uid;
|
||||
if (!uid) throw new Error("Aucun utilisateur après connexion");
|
||||
const snap = await usersRef.doc(uid).get();
|
||||
const hasUserName = !!snap.data()?.userName;
|
||||
if (hasUserName) {
|
||||
navigation.reset({ index: 0, routes: [{ name: Routes.BottomTab }] });
|
||||
} else {
|
||||
navigation.reset({ index: 0, routes: [{ name: Routes.CreatePseudo }] });
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Login error", e?.message);
|
||||
setTooltip({ text: e?.message || "Connexion impossible", type: "error" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Page
|
||||
backgroundImg={background.homeBG}
|
||||
headerType="NAVIGATION"
|
||||
title="Connexion"
|
||||
hideBackButton
|
||||
>
|
||||
<View style={{ flex: 1, paddingTop: 20 }}>
|
||||
<ItemContainer height={490}>
|
||||
@@ -30,7 +57,7 @@ export default ({ navigation }) => {
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
}}
|
||||
>
|
||||
Bonjour, pseudo!
|
||||
Bonjour !
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
@@ -45,8 +72,11 @@ export default ({ navigation }) => {
|
||||
</View>
|
||||
<View style={{ gap: 20 }}>
|
||||
<Input
|
||||
placeholder="Adresse mail ou pseudo"
|
||||
label="Adresse mail ou pseudo"
|
||||
placeholder="Adresse mail"
|
||||
label="Adresse mail"
|
||||
type="email"
|
||||
value={email}
|
||||
setValue={setEmail}
|
||||
isBlur
|
||||
/>
|
||||
<View style={{ gap: 4 }}>
|
||||
@@ -54,9 +84,13 @@ export default ({ navigation }) => {
|
||||
placeholder="Mot de passe"
|
||||
label="Mot de passe"
|
||||
type="password"
|
||||
value={password}
|
||||
setValue={setPassword}
|
||||
isBlur
|
||||
/>
|
||||
<Pressable>
|
||||
<Pressable
|
||||
onPress={() => navigate(Routes.ForgotPassword)}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
@@ -70,12 +104,13 @@ export default ({ navigation }) => {
|
||||
</View>
|
||||
</View>
|
||||
<GradientButton
|
||||
title="Poursuivre la création"
|
||||
title="Se connecter"
|
||||
containerStyle={{
|
||||
width: "80%",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
onPress={() => navigate(Routes.BottomTab)}
|
||||
onPress={onLogin}
|
||||
disabled={loading || !email || !password}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
@@ -83,17 +118,6 @@ export default ({ navigation }) => {
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: Palette.gray,
|
||||
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
En t’inscrivant, tu acceptes nos conditions générales{"\n"}
|
||||
d’utilisation et notre politique de confidentialité.
|
||||
</Text>
|
||||
<Pressable onPress={() => navigate(Routes.Register)}>
|
||||
<Text
|
||||
style={{
|
||||
@@ -102,13 +126,13 @@ export default ({ navigation }) => {
|
||||
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
|
||||
}}
|
||||
>
|
||||
Tu as déjà un compte?{" "}
|
||||
Pas encore de compte?{" "}
|
||||
<Text
|
||||
style={{
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
}}
|
||||
>
|
||||
Se connecter
|
||||
Créer un compte
|
||||
</Text>
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Platform, View } from "react-native";
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Page from "../../layouts/Page";
|
||||
import { background } from "../../assets";
|
||||
import { gutters, Palette } from "../../styles";
|
||||
@@ -7,8 +7,81 @@ import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
import { BlurView } from "expo-blur";
|
||||
import EditInput from "./components/EditInput";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import { useGlobal } from "reactn";
|
||||
import firebase, { usersRef } from "../../config/firebase";
|
||||
import { goBack } from "../../navigation/NavigationService";
|
||||
import { checkIfEmailIsValid } from "../../actions/signupActions";
|
||||
import { Text } from "react-native";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
|
||||
const ChangeEmailAddress = () => {
|
||||
const [email, setEmail] = useState("");
|
||||
const [originalEmail, setOriginalEmail] = useState("");
|
||||
const [emailError, setEmailError] = useState("");
|
||||
const [, setTooltip] = useGlobal("_tooltip");
|
||||
const [currentUserData] = useGlobal("currentUserData");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentPassword, setCurrentPassword] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const authEmail = firebase.auth().currentUser?.email || "";
|
||||
const profileEmail = (currentUserData?.email || authEmail || "").trim();
|
||||
setEmail((prev) => (prev ? prev : profileEmail));
|
||||
setOriginalEmail(profileEmail);
|
||||
}, [currentUserData?.email]);
|
||||
|
||||
const onChangeEmail = (val) => {
|
||||
setEmail(val);
|
||||
const trimmed = (val || "").trim();
|
||||
if (trimmed.length === 0) {
|
||||
setEmailError("");
|
||||
return;
|
||||
}
|
||||
if (!checkIfEmailIsValid({ email: trimmed })) {
|
||||
setEmailError("Adresse email invalide");
|
||||
} else {
|
||||
setEmailError("");
|
||||
}
|
||||
};
|
||||
|
||||
const onSave = async () => {
|
||||
const val = (email || "").trim();
|
||||
if (!val) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
const user = firebase.auth().currentUser;
|
||||
const uid = user?.uid;
|
||||
if (!user || !uid) return;
|
||||
|
||||
// Reauthenticate user with current password before sensitive update
|
||||
try {
|
||||
const credential = firebase.auth.EmailAuthProvider.credential(
|
||||
user.email,
|
||||
currentPassword,
|
||||
);
|
||||
await user.reauthenticateWithCredential(credential);
|
||||
} catch (reauthErr) {
|
||||
throw {
|
||||
code: "auth/wrong-password",
|
||||
message: "Mot de passe incorrect.",
|
||||
};
|
||||
}
|
||||
|
||||
await user.updateEmail(val);
|
||||
await usersRef.doc(uid).set({ email: val }, { merge: true });
|
||||
setTooltip({ type: "success", text: "Email mis à jour" });
|
||||
goBack();
|
||||
} catch (e) {
|
||||
console.log("ChangeEmailAddress error", e?.message);
|
||||
const msg =
|
||||
e?.code === "auth/requires-recent-login"
|
||||
? "Veuillez vous reconnecter pour changer l'adresse email"
|
||||
: e?.message || "Mise à jour impossible";
|
||||
setTooltip({ type: "error", text: msg });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Page
|
||||
headerType="NAVIGATE"
|
||||
@@ -35,15 +108,49 @@ const ChangeEmailAddress = () => {
|
||||
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
||||
}
|
||||
>
|
||||
<EditInput placeholder="Adresse mail" label="Adresse mail" />
|
||||
<EditInput
|
||||
placeholder="Adresse mail"
|
||||
label="Adresse mail"
|
||||
value={email}
|
||||
setValue={onChangeEmail}
|
||||
type="email-address"
|
||||
/>
|
||||
<View style={{ height: 10 }} />
|
||||
<EditInput
|
||||
placeholder="Mot de passe actuel"
|
||||
label="Mot de passe actuel"
|
||||
value={currentPassword}
|
||||
setValue={setCurrentPassword}
|
||||
type="password"
|
||||
/>
|
||||
{!!emailError && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: Palette.red,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
marginTop: 6,
|
||||
}}
|
||||
>
|
||||
{emailError}
|
||||
</Text>
|
||||
)}
|
||||
</BlurView>
|
||||
</View>
|
||||
<GradientButton
|
||||
title="Enregistrer les modifications"
|
||||
title={loading ? "Chargement..." : "Enregistrer les modifications"}
|
||||
containerStyle={{
|
||||
width: "80%",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
onPress={onSave}
|
||||
disabled={
|
||||
loading ||
|
||||
!email?.trim() ||
|
||||
!!emailError ||
|
||||
email.trim() === originalEmail.trim() ||
|
||||
!currentPassword?.trim()
|
||||
}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { View, Text, Pressable, Platform } from "react-native";
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import Page from "../../layouts/Page";
|
||||
import { background } from "../../assets";
|
||||
import { gutters, Palette } from "../../styles";
|
||||
@@ -8,8 +8,42 @@ import { BlurView } from "expo-blur";
|
||||
import EditInput from "./components/EditInput";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import { useGlobal } from "reactn";
|
||||
import firebase from "../../config/firebase";
|
||||
import { navigate } from "../../navigation/NavigationService";
|
||||
import { Routes } from "../../navigation";
|
||||
|
||||
const ChangePassword = () => {
|
||||
const [oldPassword, setOldPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [, setTooltip] = useGlobal("_tooltip");
|
||||
|
||||
const onSave = async () => {
|
||||
const oldVal = (oldPassword || "").trim();
|
||||
const newVal = (newPassword || "").trim();
|
||||
if (!oldVal || !newVal) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
const user = firebase.auth().currentUser;
|
||||
if (!user?.email) return;
|
||||
|
||||
const credential = firebase.auth.EmailAuthProvider.credential(
|
||||
user.email,
|
||||
oldVal,
|
||||
);
|
||||
await user.reauthenticateWithCredential(credential);
|
||||
await user.updatePassword(newVal);
|
||||
setTooltip({ type: "success", text: "Mot de passe mis à jour" });
|
||||
navigate(Routes.Settings);
|
||||
} catch (e) {
|
||||
console.log("ChangePassword error", e?.message);
|
||||
const msg = e?.message || "Mise à jour impossible";
|
||||
setTooltip({ type: "error", text: msg });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Page
|
||||
headerType="NAVIGATE"
|
||||
@@ -38,9 +72,18 @@ const ChangePassword = () => {
|
||||
}
|
||||
>
|
||||
<EditInput
|
||||
placeholder="Mot de passe"
|
||||
label="Mot de passe"
|
||||
placeholder="Mot de passe actuel"
|
||||
label="Mot de passe actuel"
|
||||
type="password"
|
||||
value={oldPassword}
|
||||
setValue={setOldPassword}
|
||||
/>
|
||||
<EditInput
|
||||
placeholder="Nouveau mot de passe"
|
||||
label="Nouveau mot de passe"
|
||||
type="password"
|
||||
value={newPassword}
|
||||
setValue={setNewPassword}
|
||||
/>
|
||||
<Pressable>
|
||||
<Text
|
||||
@@ -56,11 +99,13 @@ const ChangePassword = () => {
|
||||
</BlurView>
|
||||
</View>
|
||||
<GradientButton
|
||||
title="Enregistrer les modifications"
|
||||
title={loading ? "Chargement..." : "Enregistrer les modifications"}
|
||||
containerStyle={{
|
||||
width: "80%",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
onPress={onSave}
|
||||
disabled={loading || !oldPassword?.trim() || !newPassword?.trim()}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { View, Text, Image, TextInput, Platform } from "react-native";
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Page from "../../layouts/Page";
|
||||
import { background, img } from "../../assets";
|
||||
import { gutters, Palette } from "../../styles";
|
||||
@@ -10,8 +10,61 @@ import GradientButton from "../../components/GradientButton";
|
||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
import { goBack } from "../../navigation/NavigationService";
|
||||
import EditInput from "./components/EditInput";
|
||||
import { useGlobal } from "reactn";
|
||||
import firebase, { usersRef } from "../../config/firebase";
|
||||
|
||||
const EditProfile = () => {
|
||||
const [currentUserData] = useGlobal("currentUserData");
|
||||
const [, setTooltip] = useGlobal("_tooltip");
|
||||
|
||||
const [pseudo, setPseudo] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUserData?.userName && !pseudo) {
|
||||
setPseudo(currentUserData.userName);
|
||||
}
|
||||
}, [currentUserData?.userName]);
|
||||
|
||||
const onSave = async () => {
|
||||
const raw = pseudo || "";
|
||||
const val = raw.trim();
|
||||
if (!val) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const uid = firebase.auth().currentUser?.uid;
|
||||
if (!uid) return;
|
||||
|
||||
// Check uniqueness (case-insensitive)
|
||||
const lower = val.toLowerCase();
|
||||
const snap = await usersRef
|
||||
.where("userNameLower", "==", lower)
|
||||
.limit(1)
|
||||
.get();
|
||||
if (!snap.empty && snap.docs[0].id !== uid) {
|
||||
setTooltip({ text: "Ce pseudo est déjà pris", type: "error" });
|
||||
return;
|
||||
}
|
||||
|
||||
await usersRef.doc(uid).set(
|
||||
{
|
||||
userName: val,
|
||||
userNameLower: lower,
|
||||
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
|
||||
setTooltip({ text: "Pseudo mis à jour", type: "success" });
|
||||
goBack();
|
||||
} catch (e) {
|
||||
console.log("EditProfile onSave error", e?.message);
|
||||
setTooltip({ text: e?.message || "Mise à jour impossible", type: "error" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Page
|
||||
headerType="NAVIGATE"
|
||||
@@ -56,15 +109,16 @@ const EditProfile = () => {
|
||||
Modifier la photo
|
||||
</Text>
|
||||
</View>
|
||||
<EditInput label="Pseudo" placeholder="Pseudo" />
|
||||
<EditInput label="Pseudo" placeholder="Pseudo" value={pseudo} setValue={setPseudo} />
|
||||
|
||||
<GradientButton
|
||||
title="Enregistrer les modifications"
|
||||
title={loading ? "Chargement..." : "Enregistrer les modifications"}
|
||||
containerStyle={{
|
||||
width: "80%",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
onPress={goBack}
|
||||
onPress={onSave}
|
||||
disabled={loading || !pseudo?.trim() || pseudo?.trim() === (currentUserData?.userName || "")}
|
||||
/>
|
||||
</BlurView>
|
||||
</View>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import React from "reactn";
|
||||
import React, { useEffect, useGlobal } from "reactn";
|
||||
import Page from "../../layouts/Page";
|
||||
import { gutters, Palette, Style } from "../../styles";
|
||||
import { background } from "../../assets";
|
||||
@@ -8,11 +8,33 @@ import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
import { BlurView } from "expo-blur";
|
||||
import Switch from "../../components/Switch";
|
||||
import { useState } from "react";
|
||||
import firebase, { usersRef } from "../../config/firebase";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
|
||||
export default (props) => {
|
||||
const [currentUID] = useGlobal("currentUID");
|
||||
const [currentUserData] = useGlobal("currentUserData");
|
||||
const [, setTooltip] = useGlobal("_tooltip");
|
||||
|
||||
const [isSwitch, setIsSwitch] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsSwitch(!!currentUserData?.notifications);
|
||||
}, [currentUserData?.notifications]);
|
||||
|
||||
const onToggle = async (next) => {
|
||||
try {
|
||||
setIsSwitch(next);
|
||||
const uid = currentUID || firebase.auth().currentUser?.uid;
|
||||
if (!uid) return;
|
||||
await usersRef.doc(uid).set({ notifications: next }, { merge: true });
|
||||
setTooltip({ type: "success", text: "Préférence enregistrée" });
|
||||
} catch (e) {
|
||||
setIsSwitch(!next);
|
||||
setTooltip({ type: "error", text: e?.message || "Enregistrement impossible" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Page
|
||||
headerType="NAVIGATE"
|
||||
@@ -50,7 +72,7 @@ export default (props) => {
|
||||
>
|
||||
Notifications
|
||||
</Text>
|
||||
<Switch value={isSwitch} setValue={setIsSwitch} />
|
||||
<Switch value={isSwitch} setValue={onToggle} />
|
||||
</View>
|
||||
<Text
|
||||
style={{
|
||||
|
||||
+119
-18
@@ -15,15 +15,25 @@ import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
import Style, { gutters, size } from "../../styles/Style";
|
||||
import { Palette } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import BorderGradient from "../../components/BorderGradient/BorderGradient";
|
||||
import MusicCard from "../Library/components/MusicCard";
|
||||
import { useUser } from "../../providers/UserDataProvider";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { navigate } from "../../navigation/NavigationService";
|
||||
import { Routes } from "../../navigation";
|
||||
import { useGlobal } from "reactn";
|
||||
import Animated, { Easing, FadeIn, FadeOut } from "react-native-reanimated";
|
||||
import alert from "../../components/Alert";
|
||||
import { projectsRef } from "../../config/firebase";
|
||||
|
||||
const Profile = () => {
|
||||
const [selected, setSelected] = useState("Chansons");
|
||||
const [currentUserData] = useGlobal("currentUserData");
|
||||
const { userProjects } = useUser();
|
||||
const [, setTooltip] = useGlobal("_tooltip");
|
||||
const [menuTop, setMenuTop] = useState(0);
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const [selectedProjectId, setSelectedProjectId] = useState(null);
|
||||
|
||||
const onPressMenu = (item) => {
|
||||
setSelected(item);
|
||||
@@ -74,31 +84,29 @@ const Profile = () => {
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
}}
|
||||
>
|
||||
elia.mrn
|
||||
{currentUserData?.userName}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ ...Style.containerRow, gap: 10, marginTop: 10 }}>
|
||||
<View style={{ flex: 1, alignItems: "center" }}>
|
||||
<Text style={styles.value}>0</Text>
|
||||
<Text style={styles.value}>
|
||||
{currentUserData?.playlist?.length || 0}
|
||||
</Text>
|
||||
<Text style={styles.label}>playbacks</Text>
|
||||
</View>
|
||||
<View style={{ flex: 1, alignItems: "center" }}>
|
||||
<Text style={styles.value}>880</Text>
|
||||
<Text style={styles.value}>
|
||||
{currentUserData?.followBy?.length || 0}
|
||||
</Text>
|
||||
<Text style={styles.label}>abonnés</Text>
|
||||
</View>
|
||||
<View style={{ flex: 1, alignItems: "center" }}>
|
||||
<Text style={styles.value}>6</Text>
|
||||
<Text style={styles.value}>
|
||||
{currentUserData?.following?.length || 0}
|
||||
</Text>
|
||||
<Text style={styles.label}>abonnements</Text>
|
||||
</View>
|
||||
</View>
|
||||
<GradientButton
|
||||
title="Suivre"
|
||||
containerStyle={{
|
||||
width: "80%",
|
||||
alignSelf: "center",
|
||||
marginTop: 18,
|
||||
}}
|
||||
/>
|
||||
</BlurView>
|
||||
</View>
|
||||
<View
|
||||
@@ -176,20 +184,113 @@ const Profile = () => {
|
||||
{selected === "Chansons" && (
|
||||
<View style={{ flex: 1 }}>
|
||||
<FlatList
|
||||
data={Array.from({ length: 3 })}
|
||||
data={Array.isArray(userProjects) ? userProjects : []}
|
||||
contentContainerStyle={{
|
||||
gap: 10,
|
||||
}}
|
||||
renderItem={() => (
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<MusicCard
|
||||
title={item?.title || "Sans titre"}
|
||||
subtitle={currentUserData?.userName || "MusicLand"}
|
||||
imageUri={item?.coverUrl || null}
|
||||
projectId={item?.id}
|
||||
likedBy={item?.likedBy || []}
|
||||
onPress={() =>
|
||||
navigate(Routes.MusicDetails, {
|
||||
action: "userProfile",
|
||||
})
|
||||
navigate(Routes.MusicDetails, { projectId: item.id })
|
||||
}
|
||||
onPressMore={(posTop) => {
|
||||
setSelectedProjectId(item.id);
|
||||
if (showMenu) {
|
||||
if (posTop === menuTop) {
|
||||
setShowMenu(false);
|
||||
} else {
|
||||
setShowMenu(false);
|
||||
setTimeout(() => {
|
||||
setMenuTop(posTop);
|
||||
setShowMenu(true);
|
||||
}, 200);
|
||||
}
|
||||
} else {
|
||||
setMenuTop(posTop);
|
||||
setShowMenu(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{showMenu && (
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(200).easing(Easing.ease)}
|
||||
exiting={FadeOut.duration(200).easing(Easing.ease)}
|
||||
style={{ position: "absolute", right: 0, top: menuTop, zIndex: 2 }}
|
||||
>
|
||||
<BlurView
|
||||
intensity={20}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
backgroundColor: Palette.glass,
|
||||
borderRadius: 12,
|
||||
overflow: "hidden",
|
||||
gap: 10,
|
||||
}}
|
||||
experimentalBlurMethod={
|
||||
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
||||
}
|
||||
>
|
||||
<Pressable onPress={() => setShowMenu(false)}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
Annuler
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
setShowMenu(false);
|
||||
alert(
|
||||
"Confirmer la suppression",
|
||||
"Cette action supprimera définitivement ce projet.",
|
||||
[
|
||||
{ text: "Annuler", style: "cancel", onPress: () => {} },
|
||||
{
|
||||
text: "Supprimer",
|
||||
onPress: async () => {
|
||||
try {
|
||||
if (!selectedProjectId) return;
|
||||
await projectsRef.doc(selectedProjectId).delete();
|
||||
setTooltip({ type: "success", text: "Projet supprimé" });
|
||||
} catch (e) {
|
||||
setTooltip({
|
||||
type: "error",
|
||||
text: e?.message || "Suppression impossible",
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
{ cancelable: true },
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
Supprimer
|
||||
</Text>
|
||||
</Pressable>
|
||||
</BlurView>
|
||||
</Animated.View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { View, Text } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import React from "react";
|
||||
import Page from "../../layouts/Page";
|
||||
import { background } from "../../assets";
|
||||
@@ -9,6 +9,7 @@ import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { navigate } from "../../navigation/NavigationService";
|
||||
import { Routes } from "../../navigation";
|
||||
import { useUserData } from "../../providers/UserDataProvider";
|
||||
|
||||
const SETTINGS = [
|
||||
{
|
||||
@@ -42,6 +43,17 @@ const SETTINGS = [
|
||||
];
|
||||
|
||||
const Settings = () => {
|
||||
const { onSignOut } = useUserData();
|
||||
|
||||
const handleSignOut = async () => {
|
||||
try {
|
||||
await onSignOut();
|
||||
} catch (e) {
|
||||
console.log("Sign out error", e?.message);
|
||||
} finally {
|
||||
navigate(Routes.Login);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Page
|
||||
headerType="NAVIGATION"
|
||||
@@ -65,10 +77,7 @@ const Settings = () => {
|
||||
))}
|
||||
</View>
|
||||
<View style={{ width: "80%", gap: 5, alignSelf: "center" }}>
|
||||
<BorderGradientButton
|
||||
title="Déconnexion"
|
||||
onPress={() => navigate(Routes.Login)}
|
||||
/>
|
||||
<BorderGradientButton title="Déconnexion" onPress={handleSignOut} />
|
||||
<BorderGradientButton
|
||||
title="Supprimer mon profil"
|
||||
titleStyle={{
|
||||
|
||||
@@ -12,7 +12,7 @@ import { FONT_FAMILY } from "../../../styles/Fonts";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { icons } from "../../../assets";
|
||||
|
||||
const EditInput = ({ label = "", placeholder = "", type = "default" }) => {
|
||||
const EditInput = ({ label = "", placeholder = "", type = "default", value = "", setValue = () => {} }) => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -65,6 +65,8 @@ const EditInput = ({ label = "", placeholder = "", type = "default" }) => {
|
||||
flex: 1,
|
||||
}}
|
||||
keyboardType={type}
|
||||
value={value}
|
||||
onChangeText={setValue}
|
||||
{...(type === "password" && {
|
||||
secureTextEntry: !showPassword,
|
||||
})}
|
||||
|
||||
+13
-3
@@ -1,5 +1,5 @@
|
||||
import { View, Text, Pressable } from "react-native";
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import Page from "../layouts/Page";
|
||||
import { background } from "../assets";
|
||||
import { Palette } from "../styles";
|
||||
@@ -11,6 +11,8 @@ import { Routes } from "../navigation";
|
||||
import ItemContainer from "../components/ItemContainer/ItemContainer";
|
||||
|
||||
const Register = () => {
|
||||
const [email, setEmail] = useState("");
|
||||
|
||||
return (
|
||||
<Page
|
||||
backgroundImg={background.homeBG}
|
||||
@@ -29,14 +31,22 @@ const Register = () => {
|
||||
>
|
||||
Saisis ton adresse mail pour commencer
|
||||
</Text>
|
||||
<Input placeholder="Adresse mail" label="Adresse mail" isBlur />
|
||||
<Input
|
||||
placeholder="Adresse mail"
|
||||
label="Adresse mail"
|
||||
isBlur
|
||||
type="email"
|
||||
value={email}
|
||||
setValue={setEmail}
|
||||
/>
|
||||
<BorderGradientButton
|
||||
title="Créer mon compte"
|
||||
containerStyle={{
|
||||
width: "80%",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
onPress={() => navigate(Routes.CreatePassword)}
|
||||
onPress={() => navigate(Routes.CreatePassword, { email })}
|
||||
disabled={!email}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
|
||||
+17
-27
@@ -19,39 +19,29 @@ export default ({ navigation }) => {
|
||||
|
||||
const userRouting = async () => {
|
||||
try {
|
||||
if (!firebase.auth().currentUser?.uid) {
|
||||
throw new Error("No user");
|
||||
}
|
||||
const uid = firebase.auth().currentUser?.uid;
|
||||
if (!uid) throw new Error("No user");
|
||||
|
||||
const userDoc = await usersRef
|
||||
.doc(firebase.auth().currentUser?.uid)
|
||||
.get();
|
||||
|
||||
if (userDoc?.data()) {
|
||||
setCurrentUserData(userDoc.data());
|
||||
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
name: Routes.BottomTab,
|
||||
params: {
|
||||
screen: Routes.Home,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
throw new Error("No data");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const userSnap = await usersRef.doc(uid).get();
|
||||
const user = userSnap?.data() || {};
|
||||
if (userSnap.exists) setCurrentUserData(user);
|
||||
|
||||
const hasUserName = !!user?.userName;
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
name: Routes.Onboarding,
|
||||
name: hasUserName ? Routes.BottomTab : Routes.CreatePseudo,
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
// Pas d'utilisateur connecté => vers Login
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
name: Routes.Login,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ const CreatingSong = ({ active, config }) => {
|
||||
const elapsed = moment().diff(moment(startDate));
|
||||
const pct = Math.max(
|
||||
0,
|
||||
Math.min(100, Math.floor((elapsed / totalMs) * 100))
|
||||
Math.min(100, Math.floor((elapsed / totalMs) * 100)),
|
||||
);
|
||||
setProgress(pct);
|
||||
if (pct >= 100) {
|
||||
@@ -127,10 +127,11 @@ const CreatingSong = ({ active, config }) => {
|
||||
instruments: config?.instruments || [],
|
||||
tempo: config?.tempo || "",
|
||||
},
|
||||
generationStartAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
generationStartAt:
|
||||
firebase.firestore.FieldValue.serverTimestamp(),
|
||||
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true }
|
||||
{ merge: true },
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { View, Text, Image, StyleSheet } from "react-native";
|
||||
import { View, Image, StyleSheet } from "react-native";
|
||||
import React from "react";
|
||||
import { ai, background } from "../../assets";
|
||||
import Page from "../../layouts/Page";
|
||||
@@ -27,11 +27,11 @@ const FinishCompose = () => {
|
||||
>
|
||||
<BorderGradientButton
|
||||
title="Continuer plus tard"
|
||||
onPress={() => navigate(Routes.Onboarding)}
|
||||
onPress={() => navigate(Routes.Home)}
|
||||
/>
|
||||
<GradientButton
|
||||
title="Continuer la création"
|
||||
onPress={() => navigate(Routes.Onboarding)}
|
||||
onPress={() => navigate(Routes.Home)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { View, Text, Image } from "react-native";
|
||||
import React from "react";
|
||||
import { View, Text, Image, ActivityIndicator } from "react-native";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Page from "../../layouts/Page";
|
||||
import { background, icons, img } from "../../assets";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
@@ -10,27 +10,103 @@ import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import { Routes } from "../../navigation";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
import firebase from "../../config/firebase";
|
||||
|
||||
const PouchReady = () => {
|
||||
const route = useRoute();
|
||||
const projectId = route?.params?.projectId;
|
||||
const [coverUrl, setCoverUrl] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [title, setTitle] = useState("");
|
||||
const [coverStatus, setCoverStatus] = useState(null);
|
||||
const isGenerating = coverStatus === "GENERATING";
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectId) return;
|
||||
const unsub = firebase
|
||||
.firestore()
|
||||
.collection("projects")
|
||||
.doc(projectId)
|
||||
.onSnapshot((doc) => {
|
||||
const d = doc.data() || {};
|
||||
setTitle(d?.title || "");
|
||||
setCoverUrl(d?.coverUrl || null);
|
||||
setCoverStatus(d?.coverStatus || null);
|
||||
});
|
||||
return () => unsub?.();
|
||||
}, [projectId]);
|
||||
|
||||
const generateCover = async () => {
|
||||
if (!projectId) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
// Marquer le projet en génération
|
||||
await firebase
|
||||
.firestore()
|
||||
.collection("projects")
|
||||
.doc(projectId)
|
||||
.set({ coverStatus: "GENERATING" }, { merge: true });
|
||||
|
||||
// Créer une tâche pour déclencher la Cloud Function onCreate
|
||||
await firebase.firestore().collection("tasks").add({
|
||||
type: "cover",
|
||||
projectId,
|
||||
status: "PENDING",
|
||||
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Cover task error", e?.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectId) return;
|
||||
// Si pas de cover et pas déjà en génération, lancer une tâche
|
||||
if (!coverUrl && !isGenerating && !loading) {
|
||||
generateCover();
|
||||
}
|
||||
}, [projectId, coverUrl, isGenerating]);
|
||||
|
||||
return (
|
||||
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
||||
<MusicLandHeader onPressBack={goBack} progress={72} />
|
||||
<View style={{ flex: 1, marginTop: 16 }}>
|
||||
<CreateLyricsHeader
|
||||
title="Ta pochette est prête!"
|
||||
title={
|
||||
coverUrl ? "Ta pochette est prête!" : "Génération de la pochette"
|
||||
}
|
||||
subTitle="Qu’en penses-tu ?"
|
||||
/>
|
||||
<View style={{ flex: 1, ...Style.containerCenter }}>
|
||||
<View style={{ width: "80%", position: "relative" }}>
|
||||
<Image
|
||||
source={img.placeholder}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 300,
|
||||
borderRadius: 20,
|
||||
transform: [{ rotateY: "180deg" }],
|
||||
}}
|
||||
/>
|
||||
{coverUrl ? (
|
||||
<Image
|
||||
source={{ uri: coverUrl }}
|
||||
style={{ width: "100%", height: 300, borderRadius: 20 }}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 300,
|
||||
borderRadius: 20,
|
||||
backgroundColor: "#00000040",
|
||||
...Style.containerCenter,
|
||||
}}
|
||||
>
|
||||
{isGenerating || loading ? (
|
||||
<ActivityIndicator color={Palette.white} />
|
||||
) : (
|
||||
<Image
|
||||
source={img.placeholder}
|
||||
style={{ width: 120, height: 120, opacity: 0.5 }}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
@@ -46,7 +122,7 @@ const PouchReady = () => {
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
}}
|
||||
>
|
||||
Lust for Life
|
||||
{title || "Titre"}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
@@ -55,7 +131,7 @@ const PouchReady = () => {
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
Lana del Rey
|
||||
MusicLand
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
@@ -84,16 +160,14 @@ const PouchReady = () => {
|
||||
}}
|
||||
>
|
||||
<BorderGradientButton
|
||||
title="Regénérer"
|
||||
title={isGenerating ? "Génération en cours..." : "Regénérer"}
|
||||
icon={icons.stars}
|
||||
onPress={() =>
|
||||
navigate(Routes.Regenerate, {
|
||||
progress: 72,
|
||||
})
|
||||
}
|
||||
onPress={generateCover}
|
||||
disabled={isGenerating}
|
||||
/>
|
||||
<GradientButton
|
||||
title="Valider"
|
||||
title={isGenerating ? "Veuillez patienter..." : "Valider"}
|
||||
disabled={isGenerating || !coverUrl}
|
||||
onPress={() => navigate(Routes.PhotoCover)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -129,7 +129,7 @@ const SongReady = () => {
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
navigate(Routes.PouchReady);
|
||||
navigate(Routes.PouchReady, { projectId });
|
||||
} catch (e) {
|
||||
console.log("Validate error", e?.message);
|
||||
}
|
||||
|
||||
+22
-104
@@ -8,106 +8,13 @@ import { Routes } from "../../navigation";
|
||||
import { gutters } from "../../styles";
|
||||
import firebase from "../../config/firebase";
|
||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||
import useDataFromRef from "../../hooks/useDataFromRef";
|
||||
import { useUser } from "../../providers/UserDataProvider";
|
||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
|
||||
const Studio = () => {
|
||||
const { setIsLoading } = useMinuit();
|
||||
const [selected, setSelected] = useState(null);
|
||||
|
||||
const user = firebase.auth().currentUser;
|
||||
const { data: projects } = useDataFromRef({
|
||||
ref: user
|
||||
? firebase
|
||||
.firestore()
|
||||
.collection("projects")
|
||||
.where("userId", "==", user.uid)
|
||||
.orderBy("createdAt", "desc")
|
||||
: firebase
|
||||
.firestore()
|
||||
.collection("projects")
|
||||
.orderBy("createdAt", "desc"),
|
||||
simpleRef: false,
|
||||
listener: true,
|
||||
condition: true,
|
||||
});
|
||||
|
||||
async function generateMusic() {
|
||||
try {
|
||||
await setIsLoading(true);
|
||||
const { data } = await firebase
|
||||
.functions()
|
||||
.httpsCallable("music-generateMusic")({
|
||||
lyrics: [
|
||||
{
|
||||
lyrics:
|
||||
"Sous le ciel de minuit, une idée prend son envol\nUn éclat dans le noir, comme un nouveau symbole\nDes startups qui rêvent, des projets plein d'ardeur\nMinuit est là pour elles, avec cœur et saveur\nDes écrans qui s'allument, des lignes de code en fête\nL'équipe est au taquet, pas de place pour la défaite\nLe design est notre guide, l'humain notre priorité\nPour bâtir le futur, avec créativité",
|
||||
type: "couplet",
|
||||
},
|
||||
{
|
||||
lyrics:
|
||||
"Minuit, oh Minuit, on avance ensemble vers l'infini\nNotre passion digitale, c'est notre plus beau défi\nDes idées qui fusent, l'esprit d'équipe au rendez-vous\nPour que chaque projet, brille de mille feux, c'est tout\nLa joie dans nos cœurs, l'énergie à revendre\nMinuit, c'est la flamme qui nous fait bien comprendre\nQue l'innovation, c'est la clé de demain\nAvec vous, à vos côtés, main dans la main !",
|
||||
type: "refrain",
|
||||
},
|
||||
{
|
||||
lyrics:
|
||||
"Du prototype agile, au financement final\nOn vous accompagne, c'est notre idéal\nApplications mobiles, sur mesure et parfaites\nL'optimisation constante, pour des routes bien nettes\nL'accompagnement précis, le sourire sur les visages\nC'est ça notre ADN, notre plus beau message\nLa nuit ne fait que commencer, le travail est un plaisir\nEnsemble, nous allons loin, pour un grand avenir",
|
||||
type: "couplet",
|
||||
},
|
||||
{
|
||||
lyrics:
|
||||
"Minuit, oh Minuit, on avance ensemble vers l'infini\nNotre passion digitale, c'est notre plus beau défi\nDes idées qui fusent, l'esprit d'équipe au rendez-vous\nPour que chaque projet, brille de mille feux, c'est tout\nLa joie dans nos cœurs, l'énergie à revendre\nMinuit, c'est la flamme qui nous fait bien comprendre\nQue l'innovation, c'est la clé de demain\nAvec vous, à vos côtés, main dans la main !",
|
||||
type: "refrain",
|
||||
},
|
||||
],
|
||||
title: "Viens avec nous chez Minuit",
|
||||
genres: [
|
||||
"Pop : Musique commerciale destinée au grand public, accrocheuse et mélodique. Domine les charts internationaux avec des artistes ultra-médiatisés.",
|
||||
"Soul : Voix puissantes, émotion, style afro-américain. Influence pop et le R&B. Mélodieuse, émotionnelle et expressive dérivée du gospel et du R&B. authenticité émotionnelle.",
|
||||
],
|
||||
voice:
|
||||
"Un chœur gospel pour donner une dimension spirituelle et émotionnelle à la chanson.",
|
||||
instruments: [
|
||||
"Piano classique",
|
||||
"Synthétiseur",
|
||||
"Guitare acoustique",
|
||||
"Violon",
|
||||
],
|
||||
tempo: "Normal",
|
||||
});
|
||||
console.log("data", data);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
await setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function testSunoStatus() {
|
||||
try {
|
||||
await setIsLoading(true);
|
||||
const { data } = await firebase
|
||||
.functions()
|
||||
.httpsCallable("music-getSunoStatus")({
|
||||
taskId: "1b04b24a9b95a46125a73f25144b74c2",
|
||||
});
|
||||
console.log("🎵 Status data:", data);
|
||||
|
||||
if (data.success) {
|
||||
console.log("✅ Fonction getSunoStatus fonctionne correctement");
|
||||
if (data.data.isTestId) {
|
||||
console.log("ℹ️ TaskId de test détecté - réponse simulée");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("❌ Erreur getSunoStatus:", e.message || e);
|
||||
if (e.message && e.message.includes("404")) {
|
||||
console.log("ℹ️ Erreur 404 normale pour un taskId de test");
|
||||
}
|
||||
} finally {
|
||||
await setIsLoading(false);
|
||||
}
|
||||
}
|
||||
const { userProjects: projects } = useUser();
|
||||
|
||||
return (
|
||||
<Page
|
||||
@@ -207,15 +114,26 @@ const Studio = () => {
|
||||
}}
|
||||
/>
|
||||
{selected?.musicUrls?.length > 0 && (
|
||||
<GradientButton
|
||||
title="Ecouter les audios"
|
||||
containerStyle={{
|
||||
marginTop: responsiveHeight(2),
|
||||
}}
|
||||
onPress={() =>
|
||||
navigate(Routes.SongReady, { projectId: selected.id })
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<GradientButton
|
||||
title="Ecouter les audios"
|
||||
containerStyle={{
|
||||
marginTop: responsiveHeight(2),
|
||||
}}
|
||||
onPress={() =>
|
||||
navigate(Routes.SongReady, { projectId: selected.id })
|
||||
}
|
||||
/>
|
||||
<GradientButton
|
||||
title="Générer une pochette"
|
||||
containerStyle={{
|
||||
marginTop: responsiveHeight(2),
|
||||
}}
|
||||
onPress={() =>
|
||||
navigate(Routes.PouchReady, { projectId: selected.id })
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</Page>
|
||||
|
||||
@@ -1,42 +1,13 @@
|
||||
import { View } from "react-native";
|
||||
import { Image, Pressable, View } from "react-native";
|
||||
import React from "react";
|
||||
import Page from "../../layouts/Page";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import { gutters } from "../../styles";
|
||||
import { gutters, Style } from "../../styles";
|
||||
import { navigate } from "../../navigation/NavigationService";
|
||||
import { Routes } from "../../navigation";
|
||||
import firebase from "../../config/firebase";
|
||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||
|
||||
const Writing = () => {
|
||||
const { setIsLoading } = useMinuit();
|
||||
|
||||
async function generateTestLyrics() {
|
||||
try {
|
||||
await setIsLoading(true);
|
||||
const { data } = await firebase
|
||||
.functions()
|
||||
.httpsCallable("lyrics-generateLyrics")({
|
||||
objective:
|
||||
"Célébrer l’agence Minuit et mettre en avant son expertise digitale, son esprit d’équipe et sa créativité.",
|
||||
context:
|
||||
"L’agence Minuit accompagne les startups et entreprises innovantes dans la création de produits digitaux, du prototype à la version de financement, jusqu’à l’optimisation et la mise à l’échelle. Spécialisée dans le développement sur-mesure d’applications mobiles, elle valorise l’humain, le design et l’accompagnement personnalisé. Esprit nocturne, équipe passionnée.",
|
||||
emotion:
|
||||
"La Joie : expose un bonheur profond, l'émerveillement, la gratitude, satisfaction intense, énergie positive.",
|
||||
style: "Upbeat : Pour une ambiance joyeuse et rythmée.",
|
||||
audience:
|
||||
"L’équipe Minuit et ses clients fidèles, startups ambitieuses et partenaires visionnaires.",
|
||||
structure: ["couplet", "refrain", "couplet", "refrain"],
|
||||
rhymes: "Avec rimes",
|
||||
});
|
||||
console.log("data", data);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
await setIsLoading(false);
|
||||
}
|
||||
}
|
||||
import { icons } from "../../assets";
|
||||
|
||||
const Writing = ({ navigation }) => {
|
||||
return (
|
||||
<Page
|
||||
headerType="NONE"
|
||||
@@ -45,10 +16,20 @@ const Writing = () => {
|
||||
padding: gutters * 2,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
style={{ width: 24, height: 24, ...Style.containerCenter }}
|
||||
onPress={() => navigation.goBack()}
|
||||
>
|
||||
<Image
|
||||
source={icons.chevronDown}
|
||||
style={{ width: 15, height: 15, transform: [{ rotate: "90deg" }] }}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</Pressable>
|
||||
<View style={{ flex: 1, justifyContent: "flex-end" }}>
|
||||
<GradientButton
|
||||
title="Commencer"
|
||||
onPress={() => navigate(Routes.WritingLyrics)}
|
||||
onPress={() => navigate(Routes.CreateLyricsWithAi)}
|
||||
/>
|
||||
</View>
|
||||
</Page>
|
||||
|
||||
Reference in New Issue
Block a user