diff --git a/src/components/RotationBorder/RotationBorder.js b/src/components/RotationBorder/RotationBorder.js new file mode 100644 index 0000000..57eb88e --- /dev/null +++ b/src/components/RotationBorder/RotationBorder.js @@ -0,0 +1,20 @@ +import React from "react"; + +import "./rotationTestStyle.css"; + +// Web-only rotating gradient border container +// Props: +// - active: show rotating gradient when true (default) +// - style, className: optional DOM styles/classes +const RotationBorder = ({ children, style, className, active = true }) => { + const classes = ["box", active ? "a" : null, className] + .filter(Boolean) + .join(" "); + return ( +
+ {children} +
+ ); +}; + +export default RotationBorder; diff --git a/src/components/RotationBorder/rotationTestStyle.css b/src/components/RotationBorder/rotationTestStyle.css new file mode 100644 index 0000000..18c6634 --- /dev/null +++ b/src/components/RotationBorder/rotationTestStyle.css @@ -0,0 +1,42 @@ +.box { + --border-angle: 0deg; + border-radius: 12px; + width: 100%; + height: 260px; + display: flex; + justify-content: center; + align-items: center; + box-shadow: 0px 2px 4px hsl(0 0% 0% / 25%); + animation: border-angle-rotate 5s infinite linear; + border: 2px solid transparent; + position: relative; + + &.a { + background: linear-gradient(#11012f, #11012f) padding-box, + conic-gradient( + from var(--border-angle), + oklch(0.6659 0.2211 304.21), + oklch(0.6659 0.2211 304.21 / 0%), + oklch(0.6659 0.2211 304.21), + oklch(0.6659 0.2211 304.21 / 0%), + oklch(0.6659 0.2211 304.21) + + ) + border-box; + } +} + +@keyframes border-angle-rotate { + from { + --border-angle: 0deg; + } + to { + --border-angle: 360deg; + } +} + +@property --border-angle { + syntax: ""; + initial-value: 0deg; + inherits: false; +} diff --git a/src/config/initialGlobalState.js b/src/config/initialGlobalState.js index 1376e26..68b9b25 100644 --- a/src/config/initialGlobalState.js +++ b/src/config/initialGlobalState.js @@ -14,6 +14,9 @@ export default { title: "", }, + _isLoading: false, + _loadingMessage: null, + _config: { colors: { primary: Palette.primary, diff --git a/src/config/loaderMessages.js b/src/config/loaderMessages.js new file mode 100644 index 0000000..2ff900b --- /dev/null +++ b/src/config/loaderMessages.js @@ -0,0 +1,8 @@ +const loaderMessages = { + globalDefault: "", + profilePhotoUploadWeb: "", + photoCoverGenerationWeb: "", + pouchReadyGenerationWeb: "", +}; + +export default loaderMessages; diff --git a/src/hooks/useGlobalLoading.js b/src/hooks/useGlobalLoading.js new file mode 100644 index 0000000..ac0963f --- /dev/null +++ b/src/hooks/useGlobalLoading.js @@ -0,0 +1,27 @@ +import { useCallback } from "react"; +import { useGlobal } from "reactn"; + +const useGlobalLoading = () => { + const [, setIsLoading] = useGlobal("_isLoading"); + const [, setLoadingMessage] = useGlobal("_loadingMessage"); + + const setLoading = useCallback( + async (loading, { message } = {}) => { + if (loading) { + await setLoadingMessage(message ?? null); + await setIsLoading(true); + } else { + await setIsLoading(false); + await setLoadingMessage(null); + } + }, + [setIsLoading, setLoadingMessage] + ); + + return { + setLoading, + setLoadingMessage, + }; +}; + +export default useGlobalLoading; diff --git a/src/providers/LoadingProvider.js b/src/providers/LoadingProvider.js index 84734bb..de98474 100644 --- a/src/providers/LoadingProvider.js +++ b/src/providers/LoadingProvider.js @@ -1,13 +1,15 @@ +import { ActivityIndicator, Text, View } from "react-native"; import React, { useGlobal } from "reactn"; -import { ActivityIndicator, View } from "react-native"; -import { Palette } from "../styles"; import useLayoutType from "../hooks/useLayoutType"; - +import { Palette } from "../styles"; +import { FONT_FAMILY } from "../styles/Fonts"; export default ({ children }) => { const [isGlobalLoading] = useGlobal("_isLoading"); + const [loadingMessage] = useGlobal("_loadingMessage"); const { isDesktopWeb = false, isMobileWeb = false } = useLayoutType(); + const isWeb = isDesktopWeb || isMobileWeb; return ( <> @@ -30,17 +32,35 @@ export default ({ children }) => { isDesktopWeb ? { position: "fixed" } : isMobileWeb - ? { position: "fixed" } - : { position: "absolute" }, + ? { position: "fixed" } + : { position: "absolute" }, ]} > - + )} ); }; -export const LoaderIndicator = () => { - return ; +export const LoaderIndicator = ({ isWeb = false, message = null }) => { + return ( + + + {isWeb && message ? ( + + {message} + + ) : null} + + ); }; diff --git a/src/screens/Profile/Profile.js b/src/screens/Profile/Profile.js index 701c782..ade1b3c 100644 --- a/src/screens/Profile/Profile.js +++ b/src/screens/Profile/Profile.js @@ -1,8 +1,10 @@ import { useRoute } from "@react-navigation/native"; import { BlurView } from "expo-blur"; import { Image as ExpoImage } from "expo-image"; +import * as ImagePicker from "expo-image-picker"; import React, { useEffect, useMemo, useState } from "react"; import { + ActivityIndicator, FlatList, Image, Pressable, @@ -18,13 +20,19 @@ import { background, icons, img } from "../../assets"; import BorderGradient from "../../components/BorderGradient/BorderGradient"; import MoreMenu from "../../components/MoreMenu"; import PressableScale from "../../components/PressableScale"; -import { projectsRef, usersRef } from "../../config/firebase"; +import firebase, { + projectsRef, + serverTimestamp, + usersRef, +} from "../../config/firebase"; +import loaderMessages from "../../config/loaderMessages"; +import { uploadFileToFirebase } from "../../helpers/uploadToFirebase"; import useDataFromRef from "../../hooks/useDataFromRef"; import useLayoutType from "../../hooks/useLayoutType"; import useNavigateToMusicDetails from "../../hooks/useNavigateToMusicDetails"; import Page from "../../layouts/Page"; import { Routes } from "../../navigation"; -import { navigate, push } from "../../navigation/NavigationService"; +import { push } from "../../navigation/NavigationService"; import { useUser } from "../../providers/UserDataProvider"; import { Palette } from "../../styles"; import { FONT_FAMILY } from "../../styles/Fonts"; @@ -54,6 +62,8 @@ const Profile = () => { const [menuPosition, setMenuPosition] = useState(null); const [showMenu, setShowMenu] = useState(false); const [selectedProjectId, setSelectedProjectId] = useState(null); + const [updatingPhoto, setUpdatingPhoto] = useState(false); + const [webUploadMessage, setWebUploadMessage] = useState(""); const navigateToMusicDetails = useNavigateToMusicDetails(); const onPressMenu = (item) => { setSelected(item); @@ -114,6 +124,59 @@ const Profile = () => { } }; + const onChangeProfilePicture = async (message = "") => { + if (!isSelf || updatingPhoto) return; + + try { + setUpdatingPhoto(true); + setWebUploadMessage(isWeb && message ? message : ""); + const uid = currentUID; + if (!uid) return; + + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, + aspect: [4, 4], + quality: 1, + }); + + const pickedUri = result?.assets?.[0]?.uri || null; + if (!pickedUri) return; + + const { resultURI = null } = await uploadFileToFirebase({ + uri: pickedUri, + path: `users/${uid}/profilePicture.png`, + }); + + if (!resultURI) { + throw new Error("Téléversement de l'image impossible"); + } + + await usersRef.doc(uid).set( + { + profilePictureURL: resultURI, + updatedAt: serverTimestamp(), + }, + { merge: true } + ); + + const authUser = firebase.auth().currentUser; + if (authUser) { + await authUser.updateProfile({ photoURL: resultURI }); + } + + setTooltip({ text: "Photo de profil mise à jour", type: "success" }); + } catch (e) { + setTooltip({ + text: e?.message || "Erreur changement photo de profil", + type: "error", + }); + } finally { + setUpdatingPhoto(false); + setWebUploadMessage(""); + } + }; + // Composant factorisé pour afficher la liste de musiques const MusicListSection = ({ data, emptyText }) => ( @@ -286,30 +349,59 @@ const Profile = () => { )} - - + + + + {isSelf && ( + + onChangeProfilePicture( + loaderMessages.profilePhotoUploadWeb + ) + } + disabled={updatingPhoto} + hitSlop={10} + > + + {updatingPhoto ? ( + + ) : ( + + )} + + + )} + + {isWeb && updatingPhoto && webUploadMessage ? ( + {webUploadMessage} + ) : null} { useUserData(); const { setIsLoading } = useMinuit(); const isGenerating = selectedProject?.coverStatus === "GENERATING"; + const coverGenerationMessage = isWeb + ? loaderMessages.photoCoverGenerationWeb + : ""; const onChooseForegroundImage = async () => { try { @@ -102,9 +106,31 @@ const PhotoCover = () => { {isGenerating && ( - - Génération en cours. - + {isWeb ? ( + coverGenerationMessage ? ( + + {coverGenerationMessage} + + ) : null + ) : ( + + Génération en cours. + + )} )} diff --git a/src/screens/cover/PouchReady.js b/src/screens/cover/PouchReady.js index 531effd..ca73e53 100644 --- a/src/screens/cover/PouchReady.js +++ b/src/screens/cover/PouchReady.js @@ -8,6 +8,7 @@ import BorderGradientButton from "../../components/BorderGradientButton"; import GradientButton from "../../components/GradientButton"; import MusicLandHeader from "../../components/MusicLandHeader"; import firebase, { tasksRef } from "../../config/firebase"; +import loaderMessages from "../../config/loaderMessages"; import { isWeb } from "../../hooks/useLayoutType"; import Page from "../../layouts/Page"; import { Routes } from "../../navigation"; @@ -21,6 +22,9 @@ const PouchReady = () => { const { setIsLoading } = useMinuit(); const isGenerating = selectedProject?.coverStatus === "GENERATING"; const isFocused = useIsFocused(); + const coverBackgroundMessage = isWeb + ? loaderMessages.pouchReadyGenerationWeb + : ""; const generateCover = useCallback(async () => { if (!selectedProjectId) return; @@ -126,9 +130,31 @@ const PouchReady = () => { {isGenerating && ( - - Génération en cours. - + {isWeb ? ( + coverBackgroundMessage ? ( + + {coverBackgroundMessage} + + ) : null + ) : ( + + Génération en cours. + + )} )}