feat: fixes and formatter

This commit is contained in:
2026-01-12 16:01:32 +01:00
parent 85c6084351
commit 11e632acff
353 changed files with 23315 additions and 27361 deletions
+7 -7
View File
@@ -1,18 +1,18 @@
import moment from 'moment';
import moment from 'moment'
export const validateDate = (date = null) => {
if (!date) {
return null;
return null
}
if (moment(date).isValid()) {
return date;
return date
}
if (typeof date?.toDate === 'function') {
return date?.toDate();
return date?.toDate()
}
return null;
};
return null
}
export function getAge(birthDate) {
return moment().diff(birthDate.toDate(), 'years', false);
return moment().diff(birthDate.toDate(), 'years', false)
}
+12 -12
View File
@@ -2,7 +2,7 @@ import {
responsiveHeight as _responsiveHeight,
responsiveWidth as _responsiveWidth,
responsiveFontSize as _responsiveFontSize,
} from "react-native-responsive-dimensions";
} from 'react-native-responsive-dimensions'
import {
isWeb,
@@ -10,31 +10,31 @@ import {
isLargeDesktop,
isSmallDesktop,
isSuperSmallDesktop,
} from "../hooks/useLayoutType";
} from '../hooks/useLayoutType'
export const reductionCoeff = (bypass) =>
bypass ? 1 : isLargeDesktop ? 3 : isSmallDesktop ? 2.2 : isDesktop ? 2.8 : 1;
bypass ? 1 : isLargeDesktop ? 3 : isSmallDesktop ? 2.2 : isDesktop ? 2.8 : 1
export const responsiveHeight = (height, bypass = false) => {
return _responsiveHeight(height) / reductionCoeff(bypass);
};
return _responsiveHeight(height) / reductionCoeff(bypass)
}
export const responsiveWidth = (width, bypass = false) => {
return _responsiveWidth(width) / reductionCoeff(bypass);
};
return _responsiveWidth(width) / reductionCoeff(bypass)
}
export const responsiveFontSize = (fontSize, bypass = false) => {
if (isWeb) {
if (isSuperSmallDesktop) {
return fontSize * 6;
return fontSize * 6
} else if (isSmallDesktop) {
return fontSize * 7;
return fontSize * 7
} else {
return fontSize * 7.5;
return fontSize * 7.5
}
} else {
return _responsiveFontSize(fontSize) / reductionCoeff(bypass);
return _responsiveFontSize(fontSize) / reductionCoeff(bypass)
}
};
}
// test
+59 -63
View File
@@ -1,86 +1,82 @@
export function checkIfEmailIsValid({ email }) {
let regex = new RegExp(
"([!#-'*+/-9=?A-Z^-~-]+(.[!#-'*+/-9=?A-Z^-~-]+)*|\"([]!#-[^-~ \t]|(\\[\t -~]))+\")@([!#-'*+/-9=?A-Z^-~-]+(.[!#-'*+/-9=?A-Z^-~-]+)*|[[\t -Z^-~]*])"
);
return regex.test(email);
)
return regex.test(email)
}
export function checkIfPasswordIsStrongEnough({ password }) {
const reg =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{6,})/;
return reg.test(password);
const reg = /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{6,})/
return reg.test(password)
}
export const formatPhoneNumber = ({ phoneNumber = null }) => {
if (!phoneNumber) {
return null;
return null
}
let newPhoneNumber = phoneNumber;
let newPhoneNumber = phoneNumber
newPhoneNumber = newPhoneNumber
.trim()
.replace(/\s+/g, "") // remove spaces
.replace(/\D/g, ""); // remove non digits
.replace(/\s+/g, '') // remove spaces
.replace(/\D/g, '') // remove non digits
if (newPhoneNumber.startsWith("330")) {
newPhoneNumber = newPhoneNumber.substring(2);
if (newPhoneNumber.startsWith('330')) {
newPhoneNumber = newPhoneNumber.substring(2)
}
if (newPhoneNumber.startsWith("06") || newPhoneNumber.startsWith("07")) {
newPhoneNumber = `+33${newPhoneNumber.slice(1)}`;
} else if (
newPhoneNumber.startsWith("336") ||
newPhoneNumber.startsWith("337")
) {
newPhoneNumber = `+${newPhoneNumber}`;
if (newPhoneNumber.startsWith('06') || newPhoneNumber.startsWith('07')) {
newPhoneNumber = `+33${newPhoneNumber.slice(1)}`
} else if (newPhoneNumber.startsWith('336') || newPhoneNumber.startsWith('337')) {
newPhoneNumber = `+${newPhoneNumber}`
} else {
newPhoneNumber = null;
newPhoneNumber = null
}
return newPhoneNumber;
};
return newPhoneNumber
}
export function handleFirebaseError(code = "") {
export function handleFirebaseError(code = '') {
switch (code) {
case "auth/user-not-found":
return "Ce compte n'existe pas.";
case "auth/user-disabled":
return "Ce compte est désactivé. Contacte le support si besoin.";
case "auth/invalid-verification-code":
return "Ton code de validation est incorrect.";
case "auth/provider-already-linked":
return "Ce compte est déjà lié à un utilisateur.";
case "auth/invalid-credential":
case "auth/invalid-login-credential":
case "auth/invalid-login-credentials":
return "Identifiants incorrects.";
case "auth/credential-already-in-use":
return "Ce compte existe déjà ou est déjà lié.";
case "auth/operation-not-allowed":
return "Le fournisseur d'identité n'est pas disponible.";
case "auth/invalid-email":
return "Adresse e-mail invalide.";
case "auth/wrong-password":
return "Mot de passe incorrect.";
case "auth/invalid-verification-id":
return "Impossible de t'authentifier, réessaie dans quelques secondes.";
case "auth/invalid-phone-number":
return "Numéro de téléphone incorrect.";
case "auth/too-many-requests":
return "Trop de tentatives. Réessaie dans quelques minutes.";
case "auth/email-already-in-use":
return "Un compte avec cette adresse mail existe déjà.";
case "auth/missing-password":
return "Renseigne ton mot de passe.";
case "auth/weak-password":
return "Ton mot de passe est trop faible.";
case "auth/network-request-failed":
return "Problème de connexion réseau. Vérifie ta connexion et réessaie.";
case "auth/invalid-action-code":
case "auth/expired-action-code":
case "auth/missing-oob-code":
return "Ce lien de réinitialisation n'est plus valide. Demande un nouveau mot de passe.";
case "auth/missing-email":
return "Renseigne ton adresse e-mail.";
case 'auth/user-not-found':
return "Ce compte n'existe pas."
case 'auth/user-disabled':
return 'Ce compte est désactivé. Contacte le support si besoin.'
case 'auth/invalid-verification-code':
return 'Ton code de validation est incorrect.'
case 'auth/provider-already-linked':
return 'Ce compte est déjà lié à un utilisateur.'
case 'auth/invalid-credential':
case 'auth/invalid-login-credential':
case 'auth/invalid-login-credentials':
return 'Identifiants incorrects.'
case 'auth/credential-already-in-use':
return 'Ce compte existe déjà ou est déjà lié.'
case 'auth/operation-not-allowed':
return "Le fournisseur d'identité n'est pas disponible."
case 'auth/invalid-email':
return 'Adresse e-mail invalide.'
case 'auth/wrong-password':
return 'Mot de passe incorrect.'
case 'auth/invalid-verification-id':
return "Impossible de t'authentifier, réessaie dans quelques secondes."
case 'auth/invalid-phone-number':
return 'Numéro de téléphone incorrect.'
case 'auth/too-many-requests':
return 'Trop de tentatives. Réessaie dans quelques minutes.'
case 'auth/email-already-in-use':
return 'Un compte avec cette adresse mail existe déjà.'
case 'auth/missing-password':
return 'Renseigne ton mot de passe.'
case 'auth/weak-password':
return 'Ton mot de passe est trop faible.'
case 'auth/network-request-failed':
return 'Problème de connexion réseau. Vérifie ta connexion et réessaie.'
case 'auth/invalid-action-code':
case 'auth/expired-action-code':
case 'auth/missing-oob-code':
return "Ce lien de réinitialisation n'est plus valide. Demande un nouveau mot de passe."
case 'auth/missing-email':
return 'Renseigne ton adresse e-mail.'
default:
return "Une erreur est survenue. Réessaie dans quelques instants.";
return 'Une erreur est survenue. Réessaie dans quelques instants.'
}
}
+4 -4
View File
@@ -1,5 +1,5 @@
import * as React from "react";
import Svg, { Circle, Path } from "react-native-svg";
import * as React from 'react'
import Svg, { Circle, Path } from 'react-native-svg'
function EyeSVG(props) {
return (
@@ -19,7 +19,7 @@ function EyeSVG(props) {
<Path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z" />
<Circle cx={12} cy={12} r={3} />
</Svg>
);
)
}
export default EyeSVG;
export default EyeSVG
+4 -4
View File
@@ -1,5 +1,5 @@
import * as React from "react";
import Svg, { Path } from "react-native-svg";
import * as React from 'react'
import Svg, { Path } from 'react-native-svg'
function EyeSlashSVG(props) {
return (
@@ -21,7 +21,7 @@ function EyeSlashSVG(props) {
<Path d="M12 9a3 3 0 013 3" />
<Path d="M9.88 9.88A3 3 0 0012 15a3 3 0 002.12-.88" />
</Svg>
);
)
}
export default EyeSlashSVG;
export default EyeSlashSVG
+10 -10
View File
@@ -1,11 +1,11 @@
import * as React from "react";
import { View } from "react-native";
import Svg, { ClipPath, Defs, G, Path, Rect } from "react-native-svg";
import * as React from 'react'
import { View } from 'react-native'
import Svg, { ClipPath, Defs, G, Path, Rect } from 'react-native-svg'
const RestartSpinnerIcon = ({
size = 30,
color = "#F94697",
background = "transparent",
color = '#F94697',
background = 'transparent',
style,
}) => {
return (
@@ -15,8 +15,8 @@ const RestartSpinnerIcon = ({
width: size + 8,
height: size + 8,
borderRadius: size,
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
backgroundColor: background,
},
style,
@@ -55,7 +55,7 @@ const RestartSpinnerIcon = ({
</Defs>
</Svg>
</View>
);
};
)
}
export default RestartSpinnerIcon;
export default RestartSpinnerIcon
+125 -127
View File
@@ -1,110 +1,110 @@
import addTab from "./UI/tabs/add.png";
import albums from "./UI/tabs/albums.png";
import chat from "./UI/tabs/chat.png";
import design from "./UI/tabs/design.png";
import home from "./UI/tabs/home.png";
import mic from "./UI/tabs/mic.png";
import person from "./UI/tabs/person.png";
import quotes from "./UI/tabs/quotes.png";
import ribbon from "./UI/tabs/ribbon.png";
import settings from "./UI/tabs/settings.png";
import tasks from "./UI/tabs/tasks.png";
import addTab from './UI/tabs/add.png'
import albums from './UI/tabs/albums.png'
import chat from './UI/tabs/chat.png'
import design from './UI/tabs/design.png'
import home from './UI/tabs/home.png'
import mic from './UI/tabs/mic.png'
import person from './UI/tabs/person.png'
import quotes from './UI/tabs/quotes.png'
import ribbon from './UI/tabs/ribbon.png'
import settings from './UI/tabs/settings.png'
import tasks from './UI/tabs/tasks.png'
import bell from "./UI/bell.png";
import sort from "./UI/sort.png";
import bell from './UI/bell.png'
import sort from './UI/sort.png'
import arrowRight from "./UI/arrowRight.png";
import chevronDown from "./UI/chevronDown.png";
import threeDots from "./UI/threeDots.png";
import arrowRight from './UI/arrowRight.png'
import chevronDown from './UI/chevronDown.png'
import threeDots from './UI/threeDots.png'
import thumbDown from "./UI/thumbDown.png";
import thumbUp from "./UI/thumbUp.png";
import thumbDown from './UI/thumbDown.png'
import thumbUp from './UI/thumbUp.png'
import add from "./UI/add.png";
import addFile from "./UI/addFile.png";
import check from "./UI/check.png";
import checkCircle from "./UI/checkCircle.png";
import deliveryTime from "./UI/deliveryTime.png";
import edit from "./UI/edit.png";
import eye from "./UI/eye.png";
import lock from "./UI/lock.png";
import message from "./UI/message.png";
import search from "./UI/search.png";
import send from "./UI/send.png";
import shoppingBag from "./UI/shoppingBag.png";
import tools from "./UI/tools.png";
import trash from "./UI/trash.png";
import undo from "./UI/undo.png";
import add from './UI/add.png'
import addFile from './UI/addFile.png'
import check from './UI/check.png'
import checkCircle from './UI/checkCircle.png'
import deliveryTime from './UI/deliveryTime.png'
import edit from './UI/edit.png'
import eye from './UI/eye.png'
import lock from './UI/lock.png'
import message from './UI/message.png'
import search from './UI/search.png'
import send from './UI/send.png'
import shoppingBag from './UI/shoppingBag.png'
import tools from './UI/tools.png'
import trash from './UI/trash.png'
import undo from './UI/undo.png'
import focus from "./UI/focus.png";
import picture from "./UI/picture.png";
import screenshot from "./UI/screenshot.png";
import focus from './UI/focus.png'
import picture from './UI/picture.png'
import screenshot from './UI/screenshot.png'
import android from "./UI/android.png";
import ios from "./UI/ios.png";
import web from "./UI/web.png";
import android from './UI/android.png'
import ios from './UI/ios.png'
import web from './UI/web.png'
import law from "./UI/law.png";
import support from "./UI/support.png";
import user from "./UI/user.png";
import law from './UI/law.png'
import support from './UI/support.png'
import user from './UI/user.png'
import attachment from "./UI/attachment.png";
import attachment from './UI/attachment.png'
import cloud from "./icons/cloud.png";
import dashboard from "./icons/dashboard.png";
import file from "./icons/file.png";
import cloud from './icons/cloud.png'
import dashboard from './icons/dashboard.png'
import file from './icons/file.png'
import algolia from "./icons/algolia.png";
import calendar from "./icons/calendar.png";
import chatBubble from "./icons/chatBubble.png";
import close from "./icons/close.png";
import coin from "./icons/coin.png";
import disk from "./icons/disk.png";
import figma from "./icons/figma.png";
import forward from "./icons/forward.png";
import gitlab from "./icons/gitlab.png";
import heart from "./icons/heart.png";
import heartOutline from "./icons/heartOutline.png";
import hitParadeLogo from "./icons/hitParadeLogo.png";
import more from "./icons/more.png";
import musicLandAccueil from "./icons/musicLandAccueil.png";
import musicLandLogo from "./icons/musicLandLogo.png";
import musicLandProduction from "./icons/musicLandProduction.png";
import musicLandStudio from "./icons/musicLandStudio.png";
import musicLandVideo from "./icons/musicLandVideo.png";
import musicLandWriting from "./icons/musicLandWriting.png";
import pause from "./icons/pause.png";
import play from "./icons/play.png";
import share from "./icons/share.png";
import stars from "./icons/stars.png";
import algolia from './icons/algolia.png'
import calendar from './icons/calendar.png'
import chatBubble from './icons/chatBubble.png'
import close from './icons/close.png'
import coin from './icons/coin.png'
import disk from './icons/disk.png'
import figma from './icons/figma.png'
import forward from './icons/forward.png'
import gitlab from './icons/gitlab.png'
import heart from './icons/heart.png'
import heartOutline from './icons/heartOutline.png'
import hitParadeLogo from './icons/hitParadeLogo.png'
import more from './icons/more.png'
import musicLandAccueil from './icons/musicLandAccueil.png'
import musicLandLogo from './icons/musicLandLogo.png'
import musicLandProduction from './icons/musicLandProduction.png'
import musicLandStudio from './icons/musicLandStudio.png'
import musicLandVideo from './icons/musicLandVideo.png'
import musicLandWriting from './icons/musicLandWriting.png'
import pause from './icons/pause.png'
import play from './icons/play.png'
import share from './icons/share.png'
import stars from './icons/stars.png'
import hitParadeBG from "./UI/hitParadeBG.png";
import homeBG from "./UI/homeBG.png";
import libraryBG from "./UI/libraryBG.png";
import libraryBG2 from "./UI/libraryBG2.png";
import playbackBG from "./UI/playbackBG.png";
import playbackBG2 from "./UI/playbackBG2.png";
import productionBG from "./UI/productionBG.png";
import productionBG2 from "./UI/productionBG2.png";
import profileBG from "./UI/profileBG.png";
import studioBG from "./UI/studioBG.png";
import studioBG2 from "./UI/studioBG2.png";
import writingBG from "./UI/writingBG.png";
import hitParadeBG from './UI/hitParadeBG.png'
import homeBG from './UI/homeBG.png'
import libraryBG from './UI/libraryBG.png'
import libraryBG2 from './UI/libraryBG2.png'
import playbackBG from './UI/playbackBG.png'
import playbackBG2 from './UI/playbackBG2.png'
import productionBG from './UI/productionBG.png'
import productionBG2 from './UI/productionBG2.png'
import profileBG from './UI/profileBG.png'
import studioBG from './UI/studioBG.png'
import studioBG2 from './UI/studioBG2.png'
import writingBG from './UI/writingBG.png'
import bena from "./UI/bena.png";
import john from "./UI/john.png";
import malik from "./UI/malik.png";
import nathalie from "./UI/nathalie.png";
import theo from "./UI/theo.png";
import bena from './UI/bena.png'
import john from './UI/john.png'
import malik from './UI/malik.png'
import nathalie from './UI/nathalie.png'
import theo from './UI/theo.png'
import { Platform } from "react-native";
import goodVibe from "./UI/goodVibe.png";
import placeholder from "./UI/placeholder.jpg";
import placeholder2 from "./UI/placeholder2.jpg";
import placeholder3 from "./UI/placeholder3.png";
import placeholder4 from "./UI/placeholder4.jpg";
import profile from "./UI/profile.jpg";
import musiclandClub from "./UI/musiclandClub.png";
import { Platform } from 'react-native'
import goodVibe from './UI/goodVibe.png'
import placeholder from './UI/placeholder.jpg'
import placeholder2 from './UI/placeholder2.jpg'
import placeholder3 from './UI/placeholder3.png'
import placeholder4 from './UI/placeholder4.jpg'
import profile from './UI/profile.jpg'
import musiclandClub from './UI/musiclandClub.png'
export const tabs = {
home,
tasks,
@@ -117,12 +117,12 @@ export const tabs = {
ribbon,
mic,
person,
};
}
export const icons = {
bell,
sort,
dragDots: require("./icons/dragDots.png"),
dragDots: require('./icons/dragDots.png'),
chevronDown,
arrowRight,
@@ -186,13 +186,13 @@ export const icons = {
hitParadeLogo,
calendar,
coin,
club: require("./icons/club.png"),
clubIcon: require("./icons/clubIcon.png"),
};
club: require('./icons/club.png'),
clubIcon: require('./icons/clubIcon.png'),
}
export const background = {
writingBG,
writingBgWeb: require("./UI/writingBgWeb.png"),
writingBgWeb: require('./UI/writingBgWeb.png'),
studioBG,
studioBG2,
productionBG,
@@ -200,21 +200,21 @@ export const background = {
playbackBG,
playbackBG2,
libraryBG,
libraryBgWeb: require("./UI/libraryBgWeb.png"),
libraryBgWeb: require('./UI/libraryBgWeb.png'),
libraryBG2,
libraryBG2Web: require("./UI/libraryBG2Web.png"),
libraryBG2Web: require('./UI/libraryBG2Web.png'),
profileBG,
profileBgWeb: require("./UI/profileBgWeb.png"),
profileBgWeb: require('./UI/profileBgWeb.png'),
hitParadeBG,
hitParadeBG2: require("./UI/hitparadeBG2.jpg"),
playbackWeb: require("./UI/playbackWeb.png"),
playbackMobile: require("./UI/playbackMobile.png"),
hitParadeBG2: require('./UI/hitparadeBG2.jpg'),
playbackWeb: require('./UI/playbackWeb.png'),
playbackMobile: require('./UI/playbackMobile.png'),
homeBG,
homeBGWeb: require("./UI/homeBGWeb.png"),
loginBgWeb: require("./UI/loginBgWeb.png"),
profileWebBG: require("./UI/profileWebBG.jpg"),
bgTrans: require("./UI/bgTrans.png"),
};
homeBGWeb: require('./UI/homeBGWeb.png'),
loginBgWeb: require('./UI/loginBgWeb.png'),
profileWebBG: require('./UI/profileWebBG.jpg'),
bgTrans: require('./UI/bgTrans.png'),
}
export const ai = {
nathalie,
@@ -222,15 +222,13 @@ export const ai = {
malik,
bena,
john,
};
}
export const videos = {
test:
Platform.OS === "web"
? require("./video/testVideoWeb.mp4")
: require("./video/testVideo.mp4"),
club: require("./video/club.mp4"),
};
Platform.OS === 'web' ? require('./video/testVideoWeb.mp4') : require('./video/testVideo.mp4'),
club: require('./video/club.mp4'),
}
export const img = {
placeholder,
@@ -240,17 +238,17 @@ export const img = {
profile,
goodVibe,
musiclandClub,
};
}
export const cardsImg = {
writing: require("./icons/writing.png"),
studio: require("./icons/studio.png"),
video: require("./icons/video.png"),
production: require("./icons/production.png"),
};
writing: require('./icons/writing.png'),
studio: require('./icons/studio.png'),
video: require('./icons/video.png'),
production: require('./icons/production.png'),
}
export const subBadges = {
starter: require("./icons/starterBadge.png"),
pro: require("./icons/proBadge.png"),
premium: require("./icons/premiumBadge.png"),
};
starter: require('./icons/starterBadge.png'),
pro: require('./icons/proBadge.png'),
premium: require('./icons/premiumBadge.png'),
}
+5 -6
View File
@@ -1,9 +1,8 @@
import {Dimensions} from 'react-native';
import {resWidth} from '../styles';
import { Dimensions } from 'react-native'
import { resWidth } from '../styles'
const {width: DIMENSION_WIDTH, height: DIMENSION_HEIGHT} =
Dimensions.get('screen');
const { width: DIMENSION_WIDTH, height: DIMENSION_HEIGHT } = Dimensions.get('screen')
const BOTTOM_BAR_HEIGHT = resWidth(80);
const BOTTOM_BAR_HEIGHT = resWidth(80)
export {DIMENSION_WIDTH, DIMENSION_HEIGHT, BOTTOM_BAR_HEIGHT};
export { DIMENSION_WIDTH, DIMENSION_HEIGHT, BOTTOM_BAR_HEIGHT }
+1 -1
View File
@@ -1 +1 @@
export * from './constants';
export * from './constants'
+7 -7
View File
@@ -1,10 +1,10 @@
import { View } from "react-native";
import { View } from 'react-native'
import { LoaderIndicator } from "../providers/LoadingProvider";
import { LoaderIndicator } from '../providers/LoadingProvider'
export default ({
message,
defaultMessage = "Chargement...",
defaultMessage = 'Chargement...',
containerStyle = {},
indicatorProps = {},
messageStyle = {},
@@ -13,8 +13,8 @@ export default ({
<View
style={[
{
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
},
containerStyle,
]}
@@ -26,5 +26,5 @@ export default ({
indicatorProps={indicatorProps}
/>
</View>
);
};
)
}
+63 -76
View File
@@ -1,56 +1,48 @@
import { PortalProvider } from "@gorhom/portal";
import React, { useState } from "react";
import { Alert, Platform, StyleSheet, Text, View } from "react-native";
import { PortalProvider } from '@gorhom/portal'
import React, { useState } from 'react'
import { Alert, Platform, StyleSheet, Text, View } from 'react-native'
import { BlurView } from "expo-blur";
import { Fonts, Palette, gutters } from "../styles";
import GradientButton from "./GradientButton";
import { LinearGradient } from "./LinearGradient/LinearGradient";
import Overlay from "./Overlay";
import { BlurView } from 'expo-blur'
import { Fonts, Palette, gutters } from '../styles'
import GradientButton from './GradientButton'
import { LinearGradient } from './LinearGradient/LinearGradient'
import Overlay from './Overlay'
const WebAlertModal = ({ title, description, options }) => {
const [visible, setVisible] = useState(true);
const [visible, setVisible] = useState(true)
const confirmOption = options?.find(({ style }) => style !== "cancel");
const cancelOption = options?.find(({ style }) => style === "cancel");
const hasSecondaryAction = Boolean(cancelOption);
const buttonContainerStyle = hasSecondaryAction
? styles.actionButton
: styles.singleActionButton;
const confirmOption = options?.find(({ style }) => style !== 'cancel')
const cancelOption = options?.find(({ style }) => style === 'cancel')
const hasSecondaryAction = Boolean(cancelOption)
const buttonContainerStyle = hasSecondaryAction ? styles.actionButton : styles.singleActionButton
const onConfirm = () => {
setVisible(false);
confirmOption?.onPress();
};
setVisible(false)
confirmOption?.onPress()
}
const onCancel = () => {
setVisible(false);
cancelOption?.onPress();
};
setVisible(false)
cancelOption?.onPress()
}
const renderDescription = () => {
if (
typeof description === "string" ||
typeof description === "number"
) {
return <Text style={styles.description}>{description}</Text>;
if (typeof description === 'string' || typeof description === 'number') {
return <Text style={styles.description}>{description}</Text>
}
if (!description) {
return null;
return null
}
return <View style={styles.customDescription}>{description}</View>;
};
return <View style={styles.customDescription}>{description}</View>
}
return (
<PortalProvider>
<Overlay
isVisible={visible}
contentContainerStyle={styles.overlayContent}
>
<Overlay isVisible={visible} contentContainerStyle={styles.overlayContent}>
<View style={styles.modalWrapper}>
<LinearGradient
colors={["rgba(255,255,255,0.18)", "rgba(255,255,255,0.04)"]}
colors={['rgba(255,255,255,0.18)', 'rgba(255,255,255,0.04)']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.modalBorder}
@@ -59,24 +51,19 @@ const WebAlertModal = ({ title, description, options }) => {
<Text style={styles.title}>{title}</Text>
{renderDescription()}
<View style={styles.divider} />
<View
style={hasSecondaryAction ? styles.actionsRow : styles.actions}
>
<View style={hasSecondaryAction ? styles.actionsRow : styles.actions}>
{cancelOption && (
<GradientButton
title={cancelOption.text}
onPress={onCancel}
colors={[
"rgba(255,255,255,0.16)",
"rgba(255,255,255,0.08)",
]}
colors={['rgba(255,255,255,0.16)', 'rgba(255,255,255,0.08)']}
textStyle={styles.secondaryButtonText}
gradientStyle={styles.secondaryButtonGradient}
containerStyle={buttonContainerStyle}
/>
)}
<GradientButton
title={confirmOption?.text || "OK"}
title={confirmOption?.text || 'OK'}
onPress={onConfirm}
containerStyle={buttonContainerStyle}
/>
@@ -86,16 +73,16 @@ const WebAlertModal = ({ title, description, options }) => {
</View>
</Overlay>
</PortalProvider>
);
};
)
}
const alertPolyfill = (title, description, options, extra) => {
const rootDiv = document.createElement("div");
document.body.appendChild(rootDiv);
const rootDiv = document.createElement('div')
document.body.appendChild(rootDiv)
const closeModal = () => {
document.body.removeChild(rootDiv);
};
document.body.removeChild(rootDiv)
}
const WebAlertComponent = () => (
<WebAlertModal
@@ -104,38 +91,38 @@ const alertPolyfill = (title, description, options, extra) => {
options={options}
onDismiss={closeModal}
/>
);
)
// Render the React component into the div
require("react-dom").render(<WebAlertComponent />, rootDiv);
};
require('react-dom').render(<WebAlertComponent />, rootDiv)
}
const customAlert = (title, description, options, extra) => {
// Utilisation de la propriété userInterfaceStyle pour le mettre en mode sombre
Alert.alert(title, description, options, {
...extra,
userInterfaceStyle: "dark",
});
};
userInterfaceStyle: 'dark',
})
}
const alert = Platform.OS === "web" ? alertPolyfill : customAlert;
const alert = Platform.OS === 'web' ? alertPolyfill : customAlert
export const showPremiumRequiredAlert = () =>
alert(
"Accès premium requis",
"Vous devez payer pour créer une nouvelle musique."
'Accès premium requis',
'Vous devez payer pour créer une nouvelle musique.'
// [{ text: "OK", style: "cancel" }]
);
)
export default alert;
export default alert
const styles = StyleSheet.create({
overlayContent: {
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
},
modalWrapper: {
width: "90%",
width: '90%',
maxWidth: 460,
paddingHorizontal: 12,
},
@@ -147,49 +134,49 @@ const styles = StyleSheet.create({
borderRadius: 23,
paddingHorizontal: gutters * 1.8,
paddingVertical: gutters,
backgroundColor: "rgba(15, 12, 20, 0.9)",
overflow: "hidden",
backgroundColor: 'rgba(15, 12, 20, 0.9)',
overflow: 'hidden',
gap: gutters * 0.75,
},
title: Fonts({
type: "mainTitle",
type: 'mainTitle',
fontSize: 3,
style: { textAlign: "center" },
style: { textAlign: 'center' },
}),
description: Fonts({
type: "default",
type: 'default',
color: Palette.gray,
fontSize: 2,
style: { lineHeight: 22, textAlign: "center" },
style: { lineHeight: 22, textAlign: 'center' },
}),
divider: {
height: 1,
backgroundColor: "rgba(255,255,255,0.08)",
backgroundColor: 'rgba(255,255,255,0.08)',
marginVertical: 0,
},
customDescription: {
width: "100%",
width: '100%',
},
actionsRow: {
flexDirection: "row",
flexDirection: 'row',
gap: gutters,
width: "100%",
width: '100%',
},
actions: {
width: "100%",
width: '100%',
gap: gutters,
},
actionButton: {
flex: 1,
},
singleActionButton: {
width: "100%",
width: '100%',
},
secondaryButtonGradient: {
borderWidth: 1,
borderColor: "rgba(255,255,255,0.2)",
borderColor: 'rgba(255,255,255,0.2)',
},
secondaryButtonText: {
color: Palette.gray,
},
});
})
@@ -1,11 +1,11 @@
import React, { useMemo } from "react";
import { Animated, StyleSheet, useWindowDimensions, View } from "react-native";
import { Palette } from "../../styles";
import React, { useMemo } from 'react'
import { Animated, StyleSheet, useWindowDimensions, View } from 'react-native'
import { Palette } from '../../styles'
const ORIENTATION = {
HORIZONTAL: "horizontal",
VERTICAL: "vertical",
};
HORIZONTAL: 'horizontal',
VERTICAL: 'vertical',
}
const AnimatedPaginationDot = ({
data = [],
@@ -16,44 +16,32 @@ const AnimatedPaginationDot = ({
orientation = ORIENTATION.HORIZONTAL,
expandingDotSize = 20,
inactiveDotOpacity = 0.4,
inactiveDotColor = "rgba(255,255,255,0.4)",
inactiveDotColor = 'rgba(255,255,255,0.4)',
activeDotColor = Palette.white,
baseDotSize = 10,
}) => {
const animatedValue = useMemo(
() => scrollValue || new Animated.Value(0),
[scrollValue]
);
const animatedValue = useMemo(() => scrollValue || new Animated.Value(0), [scrollValue])
const { width, height } = useWindowDimensions();
const { width, height } = useWindowDimensions()
const distance = useMemo(() => {
if (typeof itemDimension === "number" && itemDimension > 0) {
return itemDimension;
if (typeof itemDimension === 'number' && itemDimension > 0) {
return itemDimension
}
if (orientation === ORIENTATION.VERTICAL) {
return Math.max(height, 1);
return Math.max(height, 1)
}
return Math.max(width, 1);
}, [height, itemDimension, orientation, width]);
return Math.max(width, 1)
}, [height, itemDimension, orientation, width])
const resolvedDotStyle = useMemo(
() => StyleSheet.flatten(dotStyle) || {},
[dotStyle]
);
const defaultSize = Math.max(baseDotSize, 1);
const resolvedDotStyle = useMemo(() => StyleSheet.flatten(dotStyle) || {}, [dotStyle])
const defaultSize = Math.max(baseDotSize, 1)
const baseWidth =
typeof resolvedDotStyle?.width === "number"
? resolvedDotStyle.width
: defaultSize;
typeof resolvedDotStyle?.width === 'number' ? resolvedDotStyle.width : defaultSize
const baseHeight =
typeof resolvedDotStyle?.height === "number"
? resolvedDotStyle.height
: defaultSize;
typeof resolvedDotStyle?.height === 'number' ? resolvedDotStyle.height : defaultSize
const containerOrientationStyle =
orientation === ORIENTATION.VERTICAL
? styles.containerVertical
: styles.containerHorizontal;
orientation === ORIENTATION.VERTICAL ? styles.containerVertical : styles.containerHorizontal
return (
<View
@@ -61,28 +49,24 @@ const AnimatedPaginationDot = ({
style={[styles.containerBase, containerOrientationStyle, containerStyle]}
>
{data.map((_, index) => {
const inputRange = [
(index - 1) * distance,
index * distance,
(index + 1) * distance,
];
const inputRange = [(index - 1) * distance, index * distance, (index + 1) * distance]
const staticSizeStyle = {
width: baseWidth,
height: baseHeight,
};
}
const color = animatedValue.interpolate({
inputRange,
outputRange: [inactiveDotColor, activeDotColor, inactiveDotColor],
extrapolate: "clamp",
});
extrapolate: 'clamp',
})
const opacity = animatedValue.interpolate({
inputRange,
outputRange: [inactiveDotOpacity, 1, inactiveDotOpacity],
extrapolate: "clamp",
});
extrapolate: 'clamp',
})
const primarySize = animatedValue.interpolate({
inputRange,
@@ -91,13 +75,11 @@ const AnimatedPaginationDot = ({
expandingDotSize,
orientation === ORIENTATION.VERTICAL ? baseHeight : baseWidth,
],
extrapolate: "clamp",
});
extrapolate: 'clamp',
})
const animatedSizeStyle =
orientation === ORIENTATION.VERTICAL
? { height: primarySize }
: { width: primarySize };
orientation === ORIENTATION.VERTICAL ? { height: primarySize } : { width: primarySize }
return (
<Animated.View
@@ -110,30 +92,30 @@ const AnimatedPaginationDot = ({
{ backgroundColor: color, opacity },
]}
/>
);
)
})}
</View>
);
};
)
}
AnimatedPaginationDot.orientation = ORIENTATION;
AnimatedPaginationDot.orientation = ORIENTATION
export default AnimatedPaginationDot;
export default AnimatedPaginationDot
const styles = StyleSheet.create({
containerBase: {
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
},
containerHorizontal: {
flexDirection: "row",
flexDirection: 'row',
},
containerVertical: {
flexDirection: "column",
flexDirection: 'column',
},
dotBase: {
borderRadius: 999,
marginHorizontal: 4,
marginVertical: 4,
},
});
})
+28 -34
View File
@@ -1,31 +1,25 @@
import { Portal } from "@gorhom/portal";
import { BlurView } from "expo-blur";
import { useCallback } from "react";
import { Platform, Pressable, StyleSheet, View } from "react-native";
import ActionSheet, { SheetManager } from "react-native-actions-sheet";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { isWeb } from "../hooks/useLayoutType";
import { gutters, Palette } from "../styles";
import { Portal } from '@gorhom/portal'
import { BlurView } from 'expo-blur'
import { useCallback } from 'react'
import { Platform, Pressable, StyleSheet, View } from 'react-native'
import ActionSheet, { SheetManager } from 'react-native-actions-sheet'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { isWeb } from '../hooks/useLayoutType'
import { gutters, Palette } from '../styles'
const AppActionSheet = ({
id,
children,
webModal = false,
onClose = () => {},
...sheetProps
}) => {
const insets = useSafeAreaInsets();
const AppActionSheet = ({ id, children, webModal = false, onClose = () => {}, ...sheetProps }) => {
const insets = useSafeAreaInsets()
const handleRequestClose = useCallback(() => {
if (id) {
Promise.resolve(SheetManager.hide(id))
.catch(() => {})
.finally(() => {
onClose?.();
});
return;
onClose?.()
})
return
}
onClose?.();
}, [id, onClose]);
onClose?.()
}, [id, onClose])
if (isWeb && webModal) {
return (
@@ -43,7 +37,7 @@ const AppActionSheet = ({
</View>
</View>
</Portal>
);
)
}
return (
@@ -62,7 +56,7 @@ const AppActionSheet = ({
{...sheetProps}
>
<BlurView
intensity={Platform.OS === "ios" || isWeb ? 20 : 10}
intensity={Platform.OS === 'ios' || isWeb ? 20 : 10}
style={{
paddingTop: 36,
paddingHorizontal: 14,
@@ -70,7 +64,7 @@ const AppActionSheet = ({
backgroundColor: Palette.glass,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
overflow: "hidden",
overflow: 'hidden',
}}
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
@@ -79,33 +73,33 @@ const AppActionSheet = ({
{children}
</BlurView>
</ActionSheet>
);
};
)
}
export default AppActionSheet;
export default AppActionSheet
const styles = StyleSheet.create({
webOverlay: {
...StyleSheet.absoluteFillObject,
position: "fixed",
position: 'fixed',
zIndex: 100,
},
webBackdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: "rgba(0, 0, 0, 0.55)",
backgroundColor: 'rgba(0, 0, 0, 0.55)',
},
webModalWrapper: {
flex: 1,
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
padding: 24,
},
webModalCard: {
width: 480,
maxWidth: "90%",
maxWidth: '90%',
borderRadius: 28,
overflow: "hidden",
overflow: 'hidden',
backgroundColor: Palette.glass,
padding: 32,
},
});
})
+8 -8
View File
@@ -1,8 +1,8 @@
import { View, Text, Pressable } from "react-native";
import React from "react";
import { Palette, Style } from "../styles";
import { size } from "../styles/Style";
import { FONT_FAMILY } from "../styles/Fonts";
import { View, Text, Pressable } from 'react-native'
import React from 'react'
import { Palette, Style } from '../styles'
import { size } from '../styles/Style'
import { FONT_FAMILY } from '../styles/Fonts'
const AppCheckbox = ({ onPress, selected, label }) => {
return (
@@ -42,7 +42,7 @@ const AppCheckbox = ({ onPress, selected, label }) => {
{label}
</Text>
</Pressable>
);
};
)
}
export default AppCheckbox;
export default AppCheckbox
+74 -82
View File
@@ -1,93 +1,85 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import React, { useCallback, useEffect, useState } from "react";
import {
Image,
Linking,
Platform,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
import AsyncStorage from '@react-native-async-storage/async-storage'
import React, { useCallback, useEffect, useState } from 'react'
import { Image, Linking, Platform, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
import { icons } from "../assets";
import { appleAppStoreUrl, googlePlayStoreUrl } from "../data";
import useLayoutType from "../hooks/useLayoutType";
import { Palette, gutters } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { icons } from '../assets'
import { appleAppStoreUrl, googlePlayStoreUrl } from '../data'
import useLayoutType from '../hooks/useLayoutType'
import { Palette, gutters } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
const STORAGE_KEY = "appDownloadBanner:dismissed";
const STORAGE_KEY = 'appDownloadBanner:dismissed'
const AppDownloadBanner = () => {
const { isMobileWeb } = useLayoutType();
const [isVisible, setIsVisible] = useState(false);
const [hasHydrated, setHasHydrated] = useState(false);
const { isMobileWeb } = useLayoutType()
const [isVisible, setIsVisible] = useState(false)
const [hasHydrated, setHasHydrated] = useState(false)
useEffect(() => {
let mounted = true;
let mounted = true
if (!isMobileWeb) {
setIsVisible(false);
setHasHydrated(false);
return undefined;
setIsVisible(false)
setHasHydrated(false)
return undefined
}
AsyncStorage.getItem(STORAGE_KEY)
.then((value) => {
if (!mounted) return;
setIsVisible(value !== "hidden");
setHasHydrated(true);
if (!mounted) return
setIsVisible(value !== 'hidden')
setHasHydrated(true)
})
.catch(() => {
if (!mounted) return;
setIsVisible(true);
setHasHydrated(true);
});
if (!mounted) return
setIsVisible(true)
setHasHydrated(true)
})
return () => {
mounted = false;
};
}, [isMobileWeb]);
mounted = false
}
}, [isMobileWeb])
const handleDismiss = useCallback(() => {
setIsVisible(false);
AsyncStorage.setItem(STORAGE_KEY, "hidden").catch(() => {});
}, []);
setIsVisible(false)
AsyncStorage.setItem(STORAGE_KEY, 'hidden').catch(() => {})
}, [])
const openLink = useCallback((url) => {
if (typeof url !== "string") return;
const target = url.trim();
if (!target) return;
if (typeof url !== 'string') return
const target = url.trim()
if (!target) return
if (Platform.OS === "web") {
if (Platform.OS === 'web') {
try {
window.open(target, "_blank", "noopener,noreferrer");
return;
window.open(target, '_blank', 'noopener,noreferrer')
return
} catch (error) {
console.warn("AppDownloadBanner: failed to open link in new tab", error);
console.warn('AppDownloadBanner: failed to open link in new tab', error)
}
}
Linking.openURL(target).catch((error) => {
console.warn("AppDownloadBanner: failed to open store link", error);
});
}, []);
console.warn('AppDownloadBanner: failed to open store link', error)
})
}, [])
const handleOpenStore = useCallback(
(store) => {
if (store === "ios") {
openLink(appleAppStoreUrl);
return;
if (store === 'ios') {
openLink(appleAppStoreUrl)
return
}
if (store === "android") {
openLink(googlePlayStoreUrl);
if (store === 'android') {
openLink(googlePlayStoreUrl)
}
},
[openLink],
);
[openLink]
)
if (!isMobileWeb || !isVisible || !hasHydrated) {
return null;
return null
}
return (
@@ -101,8 +93,8 @@ const AppDownloadBanner = () => {
<View style={styles.titleContent}>
<Text style={styles.title}>Télécharge l'app MusicLand</Text>
<Text style={styles.subtitle}>
Pour une expérience mobile plus fluide, utilise l'application
native et retrouve toutes les fonctionnalités.
Pour une expérience mobile plus fluide, utilise l'application native et retrouve
toutes les fonctionnalités.
</Text>
</View>
</View>
@@ -119,38 +111,38 @@ const AppDownloadBanner = () => {
<View style={styles.actions}>
<TouchableOpacity
style={[styles.cta, styles.appStoreCta]}
onPress={() => handleOpenStore("ios")}
onPress={() => handleOpenStore('ios')}
>
<Text style={styles.ctaText}>App Store</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.cta, styles.playStoreCta]}
onPress={() => handleOpenStore("android")}
onPress={() => handleOpenStore('android')}
>
<Text style={styles.ctaText}>Google Play</Text>
</TouchableOpacity>
</View>
</View>
</View>
);
};
)
}
const styles = StyleSheet.create({
container: {
position: "fixed",
position: 'fixed',
bottom: gutters,
left: gutters,
right: gutters,
alignItems: "center",
alignItems: 'center',
zIndex: 80,
},
banner: {
width: "100%",
width: '100%',
maxWidth: 520,
borderRadius: 18,
paddingVertical: 14,
paddingHorizontal: 16,
backgroundColor: "rgba(12, 10, 16, 0.95)",
backgroundColor: 'rgba(12, 10, 16, 0.95)',
borderWidth: 1,
borderColor: Palette.ultraLightWhite,
shadowColor: Palette.black,
@@ -160,29 +152,29 @@ const styles = StyleSheet.create({
elevation: 10,
},
headerRow: {
flexDirection: "row",
alignItems: "flex-start",
flexDirection: 'row',
alignItems: 'flex-start',
marginBottom: 12,
},
titleRow: {
flex: 1,
flexDirection: "row",
alignItems: "center",
flexDirection: 'row',
alignItems: 'center',
},
logoWrapper: {
width: 50,
height: 50,
borderRadius: 12,
backgroundColor: "rgba(255, 255, 255, 0.06)",
alignItems: "center",
justifyContent: "center",
backgroundColor: 'rgba(255, 255, 255, 0.06)',
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
borderColor: Palette.ultraLightWhite,
},
logo: {
width: "80%",
height: "80%",
resizeMode: "contain",
width: '80%',
height: '80%',
resizeMode: 'contain',
},
titleContent: {
flex: 1,
@@ -210,16 +202,16 @@ const styles = StyleSheet.create({
fontFamily: FONT_FAMILY.InterSemiBold,
},
actions: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
cta: {
flex: 1,
height: 46,
borderRadius: 12,
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
borderColor: Palette.ultraLightWhite,
},
@@ -237,6 +229,6 @@ const styles = StyleSheet.create({
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
},
});
})
export default AppDownloadBanner;
export default AppDownloadBanner
+20 -20
View File
@@ -1,31 +1,31 @@
import { Text, View } from "react-native";
import { Image } from "expo-image";
import { responsiveWidth } from "../actions/responsiveSizes.js";
import { formatImageURL, getInitials } from "../helpers";
import { Fonts, Palette, Style } from "../styles";
import Badge from "./Badge.js";
import { Text, View } from 'react-native'
import { Image } from 'expo-image'
import { responsiveWidth } from '../actions/responsiveSizes.js'
import { formatImageURL, getInitials } from '../helpers'
import { Fonts, Palette, Style } from '../styles'
import Badge from './Badge.js'
export default ({
name = "",
name = '',
url = null,
size = responsiveWidth(7),
containerStyle = {},
forceRawImage = false,
badge = {},
}) => {
const uniqColorById = (uniqId = "test") => {
const uniqColorById = (uniqId = 'test') => {
// Calculer un nombre unique à partir de l'ID de l'employé
let uniqueNumber = 0;
let uniqueNumber = 0
for (let i = 0; i < uniqId?.length; i++) {
uniqueNumber += uniqId.charCodeAt(i);
uniqueNumber += uniqId.charCodeAt(i)
}
// génère des couleurs pastels claires
const color = `hsl(${uniqueNumber % 360}, 100%, 90%)`;
const color = `hsl(${uniqueNumber % 360}, 100%, 90%)`
return color;
};
return color
}
return (
<View
@@ -39,14 +39,14 @@ export default ({
<View
style={{
...Style.containerRound,
width: "100%",
height: "100%",
backgroundColor: url ? "transparent" : uniqColorById(name),
width: '100%',
height: '100%',
backgroundColor: url ? 'transparent' : uniqColorById(name),
}}
>
{url ? (
<Image
cachePolicy={"memory"}
cachePolicy={'memory'}
source={{
uri: forceRawImage ? url : formatImageURL({ url, size: 200 }),
}}
@@ -59,7 +59,7 @@ export default ({
) : (
<Text
style={Fonts({
type: "section",
type: 'section',
color: Palette.darkPurple,
style: { fontSize: size / 3 },
})}
@@ -70,5 +70,5 @@ export default ({
</View>
<Badge {...badge} />
</View>
);
};
)
}
@@ -0,0 +1,16 @@
import { Image, View } from 'react-native'
import { subBadges } from '../../assets'
export default function SubscriptionAvatar({ user }) {
const subscriptionLevel = user.premiumLevel
switch (subscriptionLevel) {
case 'starter':
return <Image source={subBadges.starter} />
case 'pro':
return <Image source={subBadges.pro} />
case 'premium':
return <Image source={subBadges.premium} />
default:
return null
}
}
+10 -15
View File
@@ -1,24 +1,19 @@
import { Motion } from "@legendapp/motion";
import { Motion } from '@legendapp/motion'
import { Fonts, Palette, Style } from "../styles";
import { Fonts, Palette, Style } from '../styles'
const Badge = ({
count = 0,
size = 20,
customContent = null,
backgroundColor = null,
} = {}) => {
const Badge = ({ count = 0, size = 20, customContent = null, backgroundColor = null } = {}) => {
if (!count && !customContent) {
return null;
return null
}
return (
<Motion.View
animate={{ scale: 1 }}
initial={{ scale: 0 }}
transition={{ type: "tween", duration: 0.5 }}
transition={{ type: 'tween', duration: 0.5 }}
style={{
position: "absolute",
position: 'absolute',
top: -size / 4,
right: -size / 4,
backgroundColor: backgroundColor || Palette.red,
@@ -34,7 +29,7 @@ const Badge = ({
<Motion.Text
animate={{ scale: 1 }}
initial={{ scale: 0 }}
transition={{ type: "tween", duration: 0.5 }}
transition={{ type: 'tween', duration: 0.5 }}
style={{
...Fonts({ color: Palette.white }),
}}
@@ -43,7 +38,7 @@ const Badge = ({
</Motion.Text>
)}
</Motion.View>
);
};
)
}
export default Badge;
export default Badge
+19 -26
View File
@@ -1,40 +1,33 @@
import { Image, Pressable, Text, View } from "react-native";
import React from "reactn";
import { responsiveWidth } from "../actions/responsiveSizes.js";
import { Image, Pressable, Text, View } from 'react-native'
import React from 'reactn'
import { responsiveWidth } from '../actions/responsiveSizes.js'
import { icons } from "../assets";
import { Fonts, Style, gutters } from "../styles";
import { icons } from '../assets'
import { Fonts, Style, gutters } from '../styles'
import { Routes } from "../navigation";
import { navigate } from "../navigation/NavigationService";
import { Routes } from '../navigation'
import { navigate } from '../navigation/NavigationService'
import useLayoutType, { sidebarWidth } from "../hooks/useLayoutType.js";
import useNotifications from "../hooks/useNotifications";
import useLayoutType, { sidebarWidth } from '../hooks/useLayoutType.js'
import useNotifications from '../hooks/useNotifications'
export default ({ title = "", containerStyle = {}, bell = false }) => {
const { isDesktop } = useLayoutType();
const notificationsContext = useNotifications();
const unreadCount = notificationsContext?.unreadCount || 0;
const hasUnread = unreadCount > 0;
export default ({ title = '', containerStyle = {}, bell = false }) => {
const { isDesktop } = useLayoutType()
const notificationsContext = useNotifications()
const unreadCount = notificationsContext?.unreadCount || 0
const hasUnread = unreadCount > 0
return (
<View
style={[
Style.containerSpaceBetween,
{ marginBottom: 20, ...containerStyle },
]}
>
<View style={[Style.containerSpaceBetween, { marginBottom: 20, ...containerStyle }]}>
<View style={Style.containerRow}>
<Text
numberOfLines={1}
style={{
...Fonts({
type: "mainTitle",
type: 'mainTitle',
style: {
marginRight: 10,
maxWidth: isDesktop
? sidebarWidth - 4 * gutters
: responsiveWidth(70),
maxWidth: isDesktop ? sidebarWidth - 4 * gutters : responsiveWidth(70),
},
}),
}}
@@ -65,5 +58,5 @@ export default ({ title = "", containerStyle = {}, bell = false }) => {
</Pressable>
</View>
</View>
);
};
)
}
+15 -22
View File
@@ -1,23 +1,16 @@
import {
View,
Text,
Pressable,
StyleSheet,
Image,
Platform,
} from "react-native";
import React from "react";
import { BlurView } from "expo-blur";
import { Palette, Style } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { icons } from "../assets";
import { size } from "../styles/Style";
import { View, Text, Pressable, StyleSheet, Image, Platform } from 'react-native'
import React from 'react'
import { BlurView } from 'expo-blur'
import { Palette, Style } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
import { icons } from '../assets'
import { size } from '../styles/Style'
const BlurItemButton = ({ onPress, title = "" }) => {
const BlurItemButton = ({ onPress, title = '' }) => {
return (
<Pressable onPress={onPress}>
<BlurView
intensity={Platform.OS === "ios" ? 20 : 10}
intensity={Platform.OS === 'ios' ? 20 : 10}
style={styles.buttonContainer}
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
@@ -28,16 +21,16 @@ const BlurItemButton = ({ onPress, title = "" }) => {
source={icons.chevronDown}
style={{
...size({ size: 15 }),
transform: [{ rotate: "-90deg" }],
transform: [{ rotate: '-90deg' }],
}}
resizeMode="contain"
/>
</BlurView>
</Pressable>
);
};
)
}
export default BlurItemButton;
export default BlurItemButton
const styles = StyleSheet.create({
buttonContainer: {
@@ -45,7 +38,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 12,
height: 56,
borderRadius: 14,
overflow: "hidden",
overflow: 'hidden',
backgroundColor: Palette.glass,
},
buttonText: {
@@ -53,4 +46,4 @@ const styles = StyleSheet.create({
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
},
});
})
@@ -1,13 +1,8 @@
import { StyleSheet, View } from "react-native";
import { StyleSheet, View } from 'react-native'
import { GradientBorderView } from "../GradientBorderView";
import { GradientBorderView } from '../GradientBorderView'
const BorderGradient = ({
children,
contentContainerStyle,
gradientProps,
...props
}) => {
const BorderGradient = ({ children, contentContainerStyle, gradientProps, ...props }) => {
return (
<GradientBorderView
gradientProps={{
@@ -26,13 +21,13 @@ const BorderGradient = ({
{children}
</View>
</GradientBorderView>
);
};
)
}
export default BorderGradient;
export default BorderGradient
const styles = StyleSheet.create({
innerContainer: {
flex: 1,
},
});
})
@@ -1,5 +1,5 @@
import { StyleSheet, View } from "react-native";
import { useState } from "react";
import { StyleSheet, View } from 'react-native'
import { useState } from 'react'
const BorderGradient = ({ children, gradientProps, ...props }) => {
const defaultGradientProps = {
@@ -15,10 +15,9 @@ const BorderGradient = ({ children, gradientProps, ...props }) => {
colors: [],
useAngle: false,
angle: 0,
};
}
const { locations, end, start, useAngle, angle, onLayout, colors } =
gradientProps;
const { locations, end, start, useAngle, angle, onLayout, colors } = gradientProps
const {
style,
@@ -32,29 +31,29 @@ const BorderGradient = ({ children, gradientProps, ...props }) => {
borderLeftWidth,
borderRightWidth,
borderBottomWidth,
} = props;
} = props
const propStart = start ?? defaultGradientProps?.start;
const propEnd = end ?? defaultGradientProps?.end;
const propStart = start ?? defaultGradientProps?.start
const propEnd = end ?? defaultGradientProps?.end
const [state, setState] = useState({
width: 1,
height: 1,
});
})
const measure = (event) => {
setState({
width: event.nativeEvent.layout.width,
height: event.nativeEvent.layout.height,
});
})
if (onLayout) {
onLayout(event);
onLayout(event)
}
};
}
const getAngle = () => {
if (useAngle) {
return angle + "deg";
return angle + 'deg'
}
// Math.atan2 handles Infinity
@@ -63,26 +62,26 @@ const BorderGradient = ({ children, gradientProps, ...props }) => {
state.width * (propEnd.y - propStart.y),
state.height * (propEnd.x - propStart.x)
) +
Math.PI / 2;
return _angle + "rad";
};
Math.PI / 2
return _angle + 'rad'
}
const getColors = () =>
colors
.map((color, index) => {
const location = locations?.[index] ?? defaultGradientProps.locations;
let locationStyle = "";
const location = locations?.[index] ?? defaultGradientProps.locations
let locationStyle = ''
if (location) {
locationStyle = " " + location * 100 + "%";
locationStyle = ' ' + location * 100 + '%'
}
return color + locationStyle;
return color + locationStyle
})
.join(",");
.join(',')
return (
<View
style={{
position: "relative",
position: 'relative',
}}
>
<View
@@ -100,35 +99,34 @@ const BorderGradient = ({ children, gradientProps, ...props }) => {
borderLeftWidth,
borderRightWidth,
borderBottomWidth,
borderStyle: "solid",
borderColor: "transparent",
borderStyle: 'solid',
borderColor: 'transparent',
// borderImage: `linear-gradient(${getAngle()},${getColors()}) 1`,
background: `linear-gradient(${getAngle()},${getColors()}) border-box`,
WebkitMask:
"linear-gradient(#fff 0 0) padding-box,linear-gradient(#fff 0 0)",
WebkitMaskComposite: "xor",
maskComposite: "exclude",
overflow: "hidden",
WebkitMask: 'linear-gradient(#fff 0 0) padding-box,linear-gradient(#fff 0 0)',
WebkitMaskComposite: 'xor',
maskComposite: 'exclude',
overflow: 'hidden',
},
]}
></View>
<View style={styles.innerContainer}>{children}</View>
</View>
);
};
)
}
export default BorderGradient;
export default BorderGradient
const styles = StyleSheet.create({
innerContainer: {
flex: 1,
position: "absolute",
position: 'absolute',
top: 0,
bottom: 0,
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
right: 0,
left: 0,
overflow: "hidden",
overflow: 'hidden',
},
});
})
+27 -30
View File
@@ -1,40 +1,39 @@
import { BlurView } from "expo-blur";
import React from "react";
import { Image, Pressable, Text, View } from "react-native";
import { Palette, Style } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { size as sizeStyle } from "../styles/Style";
import BorderGradient from "./BorderGradient/BorderGradient";
import { BlurView } from 'expo-blur'
import React from 'react'
import { Image, Pressable, Text, View } from 'react-native'
import { Palette, Style } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
import { size as sizeStyle } from '../styles/Style'
import BorderGradient from './BorderGradient/BorderGradient'
const HEIGHT_BY_SIZE = {
small: 40,
medium: 50,
large: 58,
};
}
const FONT_SIZE_BY_SIZE = {
small: 13,
medium: 15,
large: 17,
};
}
const BorderGradientButton = ({
title = "Jai déjà mes paroles",
title = 'Jai déjà mes paroles',
onPress,
icon,
titleStyle,
containerStyle = {},
tint = "dark",
tint = 'dark',
disabled = false,
maxWidth = null,
size = "medium",
size = 'medium',
height = null,
}) => {
const resolvedSize = HEIGHT_BY_SIZE[size] ? size : "medium";
const buttonHeight =
typeof height === "number" ? height : HEIGHT_BY_SIZE[resolvedSize];
const fontSize = FONT_SIZE_BY_SIZE[resolvedSize];
const iconSize = resolvedSize === "small" ? 14 : 16;
const resolvedSize = HEIGHT_BY_SIZE[size] ? size : 'medium'
const buttonHeight = typeof height === 'number' ? height : HEIGHT_BY_SIZE[resolvedSize]
const fontSize = FONT_SIZE_BY_SIZE[resolvedSize]
const iconSize = resolvedSize === 'small' ? 14 : 16
return (
<Pressable
@@ -51,7 +50,7 @@ const BorderGradientButton = ({
>
<BorderGradient
gradientProps={{
colors: ["#F94697", "#7023F7"],
colors: ['#F94697', '#7023F7'],
start: { x: 0, y: 0 },
end: { x: 1, y: 0 },
locations: [0, 1],
@@ -66,27 +65,25 @@ const BorderGradientButton = ({
<View
style={{
borderRadius: 14,
overflow: "hidden",
backgroundColor: "#73737324",
width: "100%",
height: "100%",
overflow: 'hidden',
backgroundColor: '#73737324',
width: '100%',
height: '100%',
}}
>
<BlurView
intensity={40}
tint={tint}
style={{
width: "100%",
height: "100%",
width: '100%',
height: '100%',
...Style.containerCenter,
...Style.containerRow,
borderRadius: 14,
gap: 11,
}}
>
{icon && (
<Image source={icon} style={sizeStyle({ size: iconSize })} />
)}
{icon && <Image source={icon} style={sizeStyle({ size: iconSize })} />}
<Text
style={{
fontSize,
@@ -101,7 +98,7 @@ const BorderGradientButton = ({
</View>
</BorderGradient>
</Pressable>
);
};
)
}
export default BorderGradientButton;
export default BorderGradientButton
+21 -21
View File
@@ -1,18 +1,18 @@
import { BlurView } from "expo-blur";
import React from "react";
import { Image, Pressable, Text, View } from "react-native";
import { Palette, Style } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { size } from "../styles/Style";
import RotationBorder from "./RotationBorder/RotationBorder";
import { BlurView } from 'expo-blur'
import React from 'react'
import { Image, Pressable, Text, View } from 'react-native'
import { Palette, Style } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
import { size } from '../styles/Style'
import RotationBorder from './RotationBorder/RotationBorder'
const BorderGradientButton = ({
title = "Jai déjà mes paroles",
title = 'Jai déjà mes paroles',
onPress,
icon,
titleStyle,
containerStyle = {},
tint = "dark",
tint = 'dark',
disabled = false,
maxWidth = null,
}) => {
@@ -20,35 +20,35 @@ const BorderGradientButton = ({
...(maxWidth ? { maxWidth } : {}),
...containerStyle,
opacity: disabled ? 0.6 : 1,
};
}
return (
<Pressable onPress={onPress} disabled={disabled} style={pressableStyle}>
<RotationBorder
borderWidth={2}
borderRadius={14}
colors={["#F94697", "#7023F7"]}
colors={['#F94697', '#7023F7']}
style={{
height: 50,
width: "100%",
width: '100%',
zIndex: 1,
}}
>
<View
style={{
borderRadius: 14,
overflow: "hidden",
backgroundColor: "#000000b8",
width: "100%",
height: "100%",
overflow: 'hidden',
backgroundColor: '#000000b8',
width: '100%',
height: '100%',
}}
>
<BlurView
intensity={40}
tint={tint}
style={{
width: "100%",
height: "100%",
width: '100%',
height: '100%',
...Style.containerCenter,
...Style.containerRow,
borderRadius: 14,
@@ -70,7 +70,7 @@ const BorderGradientButton = ({
</View>
</RotationBorder>
</Pressable>
);
};
)
}
export default BorderGradientButton;
export default BorderGradientButton
+2 -2
View File
@@ -1,3 +1,3 @@
import BottomSheet from "@gorhom/bottom-sheet";
import BottomSheet from '@gorhom/bottom-sheet'
export default BottomSheet;
export default BottomSheet
+29 -35
View File
@@ -1,41 +1,37 @@
import React, { useImperativeHandle, useState, useRef } from "react";
import { View, TouchableOpacity, ScrollView } from "react-native";
import { Portal } from "@gorhom/portal";
import { Motion } from "@legendapp/motion";
import React, { useImperativeHandle, useState, useRef } from 'react'
import { View, TouchableOpacity, ScrollView } from 'react-native'
import { Portal } from '@gorhom/portal'
import { Motion } from '@legendapp/motion'
import { Palette } from "../../styles";
import {
isDesktop,
isLargeDesktop,
sidebarWidth,
} from "../../hooks/useLayoutType";
import { Palette } from '../../styles'
import { isDesktop, isLargeDesktop, sidebarWidth } from '../../hooks/useLayoutType'
export const SheetScrollView = ScrollView;
export const SheetBackdrop = View;
export const SheetScrollView = ScrollView
export const SheetBackdrop = View
const BottomSheet = React.forwardRef((props, ref) => {
const [showSheet, setShowSheet] = useState(false);
const [showSheet, setShowSheet] = useState(false)
const bottomSheetRef = useRef();
const bottomSheetRef = useRef()
useImperativeHandle(ref, () => ({
snapToIndex: () => {
setShowSheet(true);
setShowSheet(true)
},
expand: () => {
setShowSheet(true);
setShowSheet(true)
},
collapse: () => closeBottomSheet(),
close: () => closeBottomSheet(),
}));
}))
const closeBottomSheet = () => {
setShowSheet(false);
props.onChange(-1);
};
setShowSheet(false)
props.onChange(-1)
}
if (!showSheet) {
return null;
return null
}
return (
@@ -44,51 +40,49 @@ const BottomSheet = React.forwardRef((props, ref) => {
initial={{ right: -500, opacity: 0 }}
animate={{ right: 0, opacity: 1 }}
style={{
position: "fixed",
position: 'fixed',
right: 0,
top: 0,
left: 0,
bottom: 0,
flex: 1,
zIndex: 1000000,
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
}}
>
<TouchableOpacity
onPress={closeBottomSheet}
ref={bottomSheetRef}
style={{
position: "absolute",
position: 'absolute',
right: 0,
top: 0,
left: 0,
bottom: 0,
flex: 1,
backgroundColor: "rgba(0,0,0,0.4)",
backgroundColor: 'rgba(0,0,0,0.4)',
}}
/>
<View
style={{
position: "absolute",
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
height: "100%",
width: isDesktop
? sidebarWidth * (isLargeDesktop ? 2 : 1.5)
: "100%",
height: '100%',
width: isDesktop ? sidebarWidth * (isLargeDesktop ? 2 : 1.5) : '100%',
backgroundColor: Palette.lightPurple,
overflow: "scroll",
overflow: 'scroll',
}}
>
{props.children}
</View>
</Motion.View>
</Portal>
);
});
)
})
// TODO une croix pour fermer sur mobile web
export default BottomSheet;
export default BottomSheet
+22 -22
View File
@@ -1,42 +1,42 @@
import { AnimatePresence, Motion } from "@legendapp/motion";
import { useKeyboard } from "@react-native-community/hooks";
import { useCallback, useEffect, useState } from "react";
import { Keyboard, StyleSheet } from "react-native";
import { AnimatePresence, Motion } from '@legendapp/motion'
import { useKeyboard } from '@react-native-community/hooks'
import { useCallback, useEffect, useState } from 'react'
import { Keyboard, StyleSheet } from 'react-native'
import BottomSheet from "./BottomSheet";
import BottomSheet from './BottomSheet'
import useLayoutType from "../hooks/useLayoutType";
import { Palette } from "../styles";
import useLayoutType from '../hooks/useLayoutType'
import { Palette } from '../styles'
export default ({
children,
bottomSheetRef,
snapPoints = ["25%", "50%"],
snapPoints = ['25%', '50%'],
handleStyle = {},
...rest
}) => {
const [currentSnapPointIndex, setCurrentSnapPointIndex] = useState(0);
const [currentSnapPointIndex, setCurrentSnapPointIndex] = useState(0)
const { keyboardShown = false } = useKeyboard();
const { isWeb } = useLayoutType();
const { keyboardShown = false } = useKeyboard()
const { isWeb } = useLayoutType()
const handleSheetChanges = useCallback((index) => {
setCurrentSnapPointIndex(index);
setCurrentSnapPointIndex(index)
if (index <= 0) {
Keyboard.dismiss();
Keyboard.dismiss()
}
}, []);
}, [])
useEffect(() => {
if (bottomSheetRef.current && !isWeb && currentSnapPointIndex > 0) {
if (keyboardShown) {
bottomSheetRef.current.expand();
bottomSheetRef.current.expand()
} else {
bottomSheetRef.current.snapToIndex(1);
bottomSheetRef.current.snapToIndex(1)
}
}
}, [keyboardShown]);
}, [keyboardShown])
return (
<>
@@ -47,7 +47,7 @@ export default ({
onPress={() => bottomSheetRef?.current?.close()}
>
<Motion.View
key={"A"}
key={'A'}
style={{
flex: 1,
backgroundColor: Palette.black,
@@ -57,10 +57,10 @@ export default ({
exit={{ opacity: 0 }}
transition={{
default: {
type: "spring",
type: 'spring',
},
opacity: {
type: "timing",
type: 'timing',
},
}}
></Motion.View>
@@ -83,5 +83,5 @@ export default ({
{children}
</BottomSheet>
</>
);
};
)
}
+33 -36
View File
@@ -1,17 +1,17 @@
import { Motion } from "@legendapp/motion";
import * as Haptics from "expo-haptics";
import { Text, View } from "react-native";
import { Motion } from '@legendapp/motion'
import * as Haptics from 'expo-haptics'
import { Text, View } from 'react-native'
import { isDesktop, isMobile, isNative } from "../hooks/useLayoutType";
import { Fonts, Palette } from "../styles";
import Style, { gutters, mainBorderRadius } from "../styles/Style";
import { isDesktop, isMobile, isNative } from '../hooks/useLayoutType'
import { Fonts, Palette } from '../styles'
import Style, { gutters, mainBorderRadius } from '../styles/Style'
export default ({
type = "primary",
theme = "default", // "default" | "radioactiv"
type = 'primary',
theme = 'default', // "default" | "radioactiv"
text,
onPress = () => console.log("null"),
onPress = () => console.log('null'),
isAbsoluteBottom = false,
alternateAction = {},
@@ -23,39 +23,39 @@ export default ({
isMainDesktopPanel = false,
}) => {
const buttonWidth = alternateAction?.text ? "49%" : "100%";
const buttonWidth = alternateAction?.text ? '49%' : '100%'
return (
<View
style={{
flexDirection: "row",
justifyContent: alternateAction?.text ? "space-between" : "center",
flexDirection: 'row',
justifyContent: alternateAction?.text ? 'space-between' : 'center',
...(isAbsoluteBottom
? {
position: "absolute",
position: 'absolute',
bottom: gutters * (isMobile ? 2 : 1),
alignItems: "flex-end",
alignItems: 'flex-end',
...(isDesktop && !isMainDesktopPanel
? {
maxWidth: 600,
minWidth: 400,
alignSelf: "center",
alignSelf: 'center',
}
: {
right: gutters,
left: gutters,
alignSelf: "center",
alignSelf: 'center',
}),
}
: {
width: "100%",
width: '100%',
}),
...contentContainerStyle,
}}
>
{alternateAction?.text && alternateAction?.onPress ? (
<BaseButton
type={"secondary"}
type={'secondary'}
theme={alternateAction?.theme || theme}
text={alternateAction.text}
onPress={alternateAction.onPress}
@@ -83,45 +83,42 @@ export default ({
}}
/>
</View>
);
};
)
}
export const BaseButton = ({
type = "primary",
theme = "default",
type = 'primary',
theme = 'default',
text,
onPress = () => console.log("null"),
onPress = () => console.log('null'),
containerStyle = {},
textStyle = {},
hasShadow = false,
}) => {
const primaryColor =
theme === "radioactiv" ? Palette.radioactivGreen : Palette.primary;
const primaryColor = theme === 'radioactiv' ? Palette.radioactivGreen : Palette.primary
const primaryTransparentColor =
theme === "radioactiv"
? Palette.transparentRadioactivGreen
: Palette.transparentPrimary;
theme === 'radioactiv' ? Palette.transparentRadioactivGreen : Palette.transparentPrimary
const textColor = type === "secondary" ? primaryColor : Palette.darkPurple;
const textColor = type === 'secondary' ? primaryColor : Palette.darkPurple
return (
<Motion.Pressable
whileTap={{ scale: 0.8 }}
onPress={() => {
if (isNative) {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
}
onPress();
onPress()
}}
style={{
width: "100%",
width: '100%',
height: 50,
marginTop: gutters / 2,
...Style.containerRow,
...Style.containerCenter,
backgroundColor: primaryColor,
borderRadius: mainBorderRadius,
...(type === "secondary"
...(type === 'secondary'
? {
backgroundColor: primaryTransparentColor,
}
@@ -134,7 +131,7 @@ export const BaseButton = ({
<Text
style={{
...Fonts({
type: "default",
type: 'default',
color: textColor,
}),
...textStyle,
@@ -143,5 +140,5 @@ export const BaseButton = ({
{text}
</Text>
</Motion.Pressable>
);
};
)
}
+36 -36
View File
@@ -1,32 +1,32 @@
import { useState, useEffect } from "reactn";
import { Pressable, TextInput, View, Image, Platform } from "react-native";
import { BlurView } from "expo-blur";
import { useState, useEffect } from 'reactn'
import { Pressable, TextInput, View, Image, Platform } from 'react-native'
import { BlurView } from 'expo-blur'
import { Fonts, gutters, Palette } from "../styles";
import Style from "../styles/Style";
import { Fonts, gutters, Palette } from '../styles'
import Style from '../styles/Style'
import { chatsRef } from "../config/firebase";
import { chatsRef } from '../config/firebase'
import { icons } from "../assets";
import { icons } from '../assets'
import { isWeb } from "../hooks/useLayoutType.js";
import { isWeb } from '../hooks/useLayoutType.js'
import DocumentDropZone from "./DocumentDropZone.js";
import FilesPreview from "./FilesPreview.js";
import DocumentDropZone from './DocumentDropZone.js'
import FilesPreview from './FilesPreview.js'
const ChatInput = ({
message,
setMessage,
onSendMessage,
containerStyle = {},
placeholder = "",
placeholder = '',
chatID = null,
}) => {
const [files, setFiles] = useState([]);
const [files, setFiles] = useState([])
useEffect(() => {
setFiles([]);
}, [chatID]);
setFiles([])
}, [chatID])
const handleMessageObject = () => {
if (message.length > 0 || files.length > 0) {
@@ -34,37 +34,37 @@ const ChatInput = ({
customPayload: {
files,
},
});
setMessage("");
setFiles([]);
})
setMessage('')
setFiles([])
}
};
}
const handleKeyPress = (e) => {
if (e?.nativeEvent?.key?.toLowerCase() === "enter") {
handleMessageObject();
if (e?.nativeEvent?.key?.toLowerCase() === 'enter') {
handleMessageObject()
}
};
}
return (
<View
style={{
...Style.containerItem,
backgroundColor: Palette.transparentDarkPurple,
justifyContent: "center",
justifyContent: 'center',
borderWidth: 1,
borderColor: Palette.ultraLightWhite,
overflow: "hidden",
width: "100%",
height: "auto",
overflow: 'hidden',
width: '100%',
height: 'auto',
padding: 0,
...containerStyle,
}}
>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 30}
intensity={Platform.OS !== 'ios' ? 10 : 30}
tint="dark"
style={{ flex: 1, justifyContent: "center" }}
style={{ flex: 1, justifyContent: 'center' }}
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// }
@@ -101,13 +101,13 @@ const ChatInput = ({
value={message}
onChangeText={setMessage}
style={{
width: "85%",
...Fonts({ type: "default", style: {} }),
width: '85%',
...Fonts({ type: 'default', style: {} }),
}}
keyboardAppearance="dark"
{...(!isWeb
? {
returnKeyType: "send",
returnKeyType: 'send',
onSubmitEditing: onSendMessage,
}
: {
@@ -118,13 +118,13 @@ const ChatInput = ({
<Pressable
onPress={handleMessageObject}
style={{
position: "absolute",
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
width: 50,
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
}}
>
<Image
@@ -138,7 +138,7 @@ const ChatInput = ({
</View>
</BlurView>
</View>
);
};
)
}
export default ChatInput;
export default ChatInput
+70 -94
View File
@@ -1,78 +1,66 @@
import { useState, useRef, useEffect, useGlobal, getGlobal } from "reactn";
import {
Pressable,
TextInput,
View,
Image,
Text,
Keyboard,
FlatList,
} from "react-native";
import { responsiveHeight } from "../actions/responsiveSizes.js";
import { useDataFromRef } from "react-native-minuit/src/hooks";
import { useKeyboard } from "@react-native-community/hooks";
import moment from "moment";
import { useState, useRef, useEffect, useGlobal, getGlobal } from 'reactn'
import { Pressable, TextInput, View, Image, Text, Keyboard, FlatList } from 'react-native'
import { responsiveHeight } from '../actions/responsiveSizes.js'
import { useDataFromRef } from 'react-native-minuit/src/hooks'
import { useKeyboard } from '@react-native-community/hooks'
import moment from 'moment'
import { Fonts, gutters, Palette } from "../styles";
import Style, { bubbleStyle } from "../styles/Style";
import { Fonts, gutters, Palette } from '../styles'
import Style, { bubbleStyle } from '../styles/Style'
import firebase, { chatsRef } from "../config/firebase";
import firebase, { chatsRef } from '../config/firebase'
import { formatNameForConfidentiality } from "../helpers/index.js";
import useLayoutType from "../hooks/useLayoutType.js";
import { formatNameForConfidentiality } from '../helpers/index.js'
import useLayoutType from '../hooks/useLayoutType.js'
import TypingLoader from "./TypingLoader";
import Avatar from "./Avatar";
import RenderChatFile from "./RenderChatFile.js";
import HyperlinkContainer from "./HyperlinkContainer.js";
import TypingLoader from './TypingLoader'
import Avatar from './Avatar'
import RenderChatFile from './RenderChatFile.js'
import HyperlinkContainer from './HyperlinkContainer.js'
export default ({
chatID = null,
layout = "default", // default | taskSideBar
layout = 'default', // default | taskSideBar
containerStyle = {},
messageListContainerStyle = {},
}) => {
const [currentUID] = useGlobal("currentUID");
const [currentProjectData] = useGlobal("currentProjectData");
const [currentUID] = useGlobal('currentUID')
const [currentProjectData] = useGlobal('currentProjectData')
const [isTyping] = useState(false);
const [isTyping] = useState(false)
const flatListRef = useRef();
const flatListRef = useRef()
const { isNative } = useLayoutType();
const { keyboardShown = false } = useKeyboard();
const { isNative } = useLayoutType()
const { keyboardShown = false } = useKeyboard()
const { data: messageList } = useDataFromRef({
ref: chatID
? chatsRef
.doc(chatID)
.collection("messages")
.orderBy("createdAt", "desc")
.limit(50)
? chatsRef.doc(chatID).collection('messages').orderBy('createdAt', 'desc').limit(50)
: null,
simpleRef: false,
listener: true,
condition: chatID,
refreshArray: [chatID],
documentID: "messageID",
});
documentID: 'messageID',
})
let conversation = [
isTyping ? { senderID: "minuit.ai", userTyping: true } : null,
isTyping ? { senderID: 'minuit.ai', userTyping: true } : null,
...(messageList || []),
].filter((item) => item);
].filter((item) => item)
if (layout === "default") {
conversation = conversation.reverse();
if (layout === 'default') {
conversation = conversation.reverse()
}
useEffect(() => {
if (flatListRef?.current && isNative && keyboardShown) {
flatListRef?.current?.scrollToEnd?.({ animated: true });
flatListRef?.current?.scrollToEnd?.({ animated: true })
}
}, [keyboardShown, isNative]);
}, [keyboardShown, isNative])
return (
<View
@@ -92,33 +80,30 @@ export default ({
}}
showsVerticalScrollIndicator={false}
keyExtractor={(item, index) =>
item?.messageID
? `${item?.messageID?.toString()}-${index}`
: `no-messageID-${index}`
item?.messageID ? `${item?.messageID?.toString()}-${index}` : `no-messageID-${index}`
}
renderItem={({
item: {
createdAt = null,
senderID,
senderName = "",
senderName = '',
senderProfilePicture = null,
text = "",
text = '',
userTyping = false,
files = [],
},
index,
}) => {
const isCurrentUser = senderID === currentUID;
const isChatbot = senderID === "minuit.ai";
const isCurrentUser = senderID === currentUID
const isChatbot = senderID === 'minuit.ai'
const senderData =
currentProjectData?.teamMembers?.[senderID] || {};
const senderData = currentProjectData?.teamMembers?.[senderID] || {}
const isDayChange =
moment(createdAt?.toDate()).format("DD/MM/YYYY") !==
moment(
conversation[index - 1]?.createdAt?.toDate() || new Date()
).format("DD/MM/YYYY") || !conversation[index - 1];
moment(createdAt?.toDate()).format('DD/MM/YYYY') !==
moment(conversation[index - 1]?.createdAt?.toDate() || new Date()).format(
'DD/MM/YYYY'
) || !conversation[index - 1]
return (
<>
@@ -132,21 +117,19 @@ export default ({
<Text
style={{
...Fonts({
type: "default",
type: 'default',
color: Palette.white,
style: { textAlign: "center", opacity: 0.5 },
style: { textAlign: 'center', opacity: 0.5 },
}),
}}
>
{moment(createdAt?.toDate()).format(
"[Le] DD/MM/YYYY [à] HH:mm"
)}
{moment(createdAt?.toDate()).format('[Le] DD/MM/YYYY [à] HH:mm')}
</Text>
<View
style={{
...Style.separatorHorizontal,
width: "100%",
width: '100%',
}}
/>
</View>
@@ -156,9 +139,9 @@ export default ({
style={[
Style.containerRow,
{
flexDirection: isCurrentUser ? "row-reverse" : "row",
alignItems: "flex-end",
width: "100%",
flexDirection: isCurrentUser ? 'row-reverse' : 'row',
alignItems: 'flex-end',
width: '100%',
marginBottom: gutters / 2,
},
]}
@@ -175,21 +158,15 @@ export default ({
}
: {
marginRight: gutters / 2,
backgroundColor: "transparent",
backgroundColor: 'transparent',
borderColor: Palette.primary,
borderWidth: 1,
}),
}}
>
<Avatar
name={
isChatbot ? "m" : senderData?.name || senderName || ""
}
url={
senderData?.profilePictureURL ||
senderProfilePicture ||
null
}
name={isChatbot ? 'm' : senderData?.name || senderName || ''}
url={senderData?.profilePictureURL || senderProfilePicture || null}
size={35}
/>
</View>
@@ -197,7 +174,7 @@ export default ({
<View
style={{
alignItems: isCurrentUser ? "flex-end" : "flex-start",
alignItems: isCurrentUser ? 'flex-end' : 'flex-start',
}}
>
{files?.map((props, index) => (
@@ -209,8 +186,8 @@ export default ({
index !== files?.length - 1
? gutters / 2
: text?.length > 0 || userTyping
? gutters / 2
: 0,
? gutters / 2
: 0,
}}
/>
)) || null}
@@ -232,11 +209,11 @@ export default ({
<HyperlinkContainer>
<Text
style={Fonts({
type: "default",
type: 'default',
style: {
color: Palette.white,
textAlign: isCurrentUser ? "right" : "left",
width: "100%",
textAlign: isCurrentUser ? 'right' : 'left',
width: '100%',
},
})}
>
@@ -249,30 +226,29 @@ export default ({
</View>
</View>
</>
);
)
}}
/>
</View>
</View>
);
};
)
}
export const onSendMessage = async ({
chatID = null,
projectID = null,
message = "",
message = '',
setMessage = () => {},
setIsTyping = () => {},
customPayload = {},
}) => {
try {
const currentUID = getGlobal()?.currentUID || null;
const { name = "", profilePictureURL = null } =
getGlobal()?.currentUserData || {};
const currentUID = getGlobal()?.currentUID || null
const { name = '', profilePictureURL = null } = getGlobal()?.currentUserData || {}
if (message.length > 0 || customPayload?.files?.length > 0) {
Keyboard.dismiss();
setMessage("");
Keyboard.dismiss()
setMessage('')
const messageData = {
projectID,
@@ -282,13 +258,13 @@ export const onSendMessage = async ({
senderProfilePicture: profilePictureURL || null,
text: message,
...customPayload,
};
}
await chatsRef.doc(chatID).collection("messages").add(messageData);
await chatsRef.doc(chatID).collection('messages').add(messageData)
}
} catch (error) {
console.log(error);
console.log(error)
} finally {
setIsTyping(false);
setIsTyping(false)
}
};
}
+7 -7
View File
@@ -1,6 +1,6 @@
import React from "react";
import { Image } from "react-native";
import { icons } from "../assets";
import React from 'react'
import { Image } from 'react-native'
import { icons } from '../assets'
const CoinIcon = ({ size = 22, style }) => {
return (
@@ -10,12 +10,12 @@ const CoinIcon = ({ size = 22, style }) => {
{
width: size,
height: size,
resizeMode: "contain",
resizeMode: 'contain',
},
style,
]}
/>
);
};
)
}
export default CoinIcon;
export default CoinIcon
+14 -14
View File
@@ -1,14 +1,14 @@
import { BlurView } from "expo-blur";
import React from "react";
import { Pressable, Text } from "react-native";
import { Routes } from "../navigation";
import { navigate } from "../navigation/NavigationService";
import { Palette } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { BlurView } from 'expo-blur'
import React from 'react'
import { Pressable, Text } from 'react-native'
import { Routes } from '../navigation'
import { navigate } from '../navigation/NavigationService'
import { Palette } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
export default function ConnectBtn({ style }) {
const handlePress = () => {
navigate(Routes.Login);
};
navigate(Routes.Login)
}
return (
<Pressable
@@ -22,9 +22,9 @@ export default function ConnectBtn({ style }) {
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 15,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Text
@@ -35,9 +35,9 @@ export default function ConnectBtn({ style }) {
marginRight: 5,
}}
>
{"Se connecter"}
{'Se connecter'}
</Text>
</BlurView>
</Pressable>
);
)
}
+40 -56
View File
@@ -1,42 +1,42 @@
import React from "react";
import { StyleSheet, Text, View } from "react-native";
import CoinIcon from "./CoinIcon";
import { FONT_FAMILY } from "../styles/Fonts";
import React from 'react'
import { StyleSheet, Text, View } from 'react-native'
import CoinIcon from './CoinIcon'
import { FONT_FAMILY } from '../styles/Fonts'
const defaultFormatOptions = {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
};
}
const formatAmount = (value, options = defaultFormatOptions) => {
if (typeof value !== "number" || !Number.isFinite(value)) {
return null;
if (typeof value !== 'number' || !Number.isFinite(value)) {
return null
}
try {
return new Intl.NumberFormat("fr-FR", {
return new Intl.NumberFormat('fr-FR', {
...defaultFormatOptions,
...options,
}).format(value);
}).format(value)
} catch (_error) {
return `${value}`;
return `${value}`
}
};
}
const extractNumericValue = (value) => {
if (typeof value === "number" && Number.isFinite(value)) {
return value;
if (typeof value === 'number' && Number.isFinite(value)) {
return value
}
if (typeof value === "string") {
const parsed = Number(value);
if (typeof value === 'string') {
const parsed = Number(value)
if (Number.isFinite(parsed)) {
return parsed;
return parsed
}
}
return null;
};
return null
}
const CreditAmount = ({
value,
@@ -44,78 +44,62 @@ const CreditAmount = ({
textStyle,
iconSize = 18,
iconStyle,
iconPosition = "right",
iconPosition = 'right',
gap = 6,
showPlus = false,
formatterOptions,
accessibilityLabel,
}) => {
const numericValue = React.useMemo(
() => extractNumericValue(value),
[value],
);
const numericValue = React.useMemo(() => extractNumericValue(value), [value])
const resolvedValue =
numericValue !== null
? numericValue
: value ?? 0;
const resolvedValue = numericValue !== null ? numericValue : (value ?? 0)
const formattedValue =
numericValue !== null
? formatAmount(resolvedValue, formatterOptions) ?? `${resolvedValue}`
: typeof resolvedValue === "string"
? resolvedValue
: `${resolvedValue}`;
? (formatAmount(resolvedValue, formatterOptions) ?? `${resolvedValue}`)
: typeof resolvedValue === 'string'
? resolvedValue
: `${resolvedValue}`
const prefix =
numericValue !== null && showPlus && numericValue > 0 ? "+" : "";
const prefix = numericValue !== null && showPlus && numericValue > 0 ? '+' : ''
const a11yLabel =
accessibilityLabel || `${prefix}${formattedValue} pièces`;
const a11yLabel = accessibilityLabel || `${prefix}${formattedValue} pièces`
const containerStyles = Array.isArray(style)
? [styles.container, { gap }, ...style]
: [styles.container, { gap }, style];
: [styles.container, { gap }, style]
const textStyles = Array.isArray(textStyle)
? [styles.value, ...textStyle]
: [styles.value, textStyle];
: [styles.value, textStyle]
const iconStyles = Array.isArray(iconStyle)
? [styles.icon, ...iconStyle]
: [styles.icon, iconStyle];
: [styles.icon, iconStyle]
return (
<View
style={containerStyles}
accessibilityRole="text"
accessibilityLabel={a11yLabel}
>
{iconPosition === "left" ? (
<CoinIcon size={iconSize} style={iconStyles} />
) : null}
<View style={containerStyles} accessibilityRole="text" accessibilityLabel={a11yLabel}>
{iconPosition === 'left' ? <CoinIcon size={iconSize} style={iconStyles} /> : null}
<Text style={textStyles}>{`${prefix}${formattedValue}`}</Text>
{iconPosition === "right" ? (
<CoinIcon size={iconSize} style={iconStyles} />
) : null}
{iconPosition === 'right' ? <CoinIcon size={iconSize} style={iconStyles} /> : null}
</View>
);
};
)
}
const styles = StyleSheet.create({
container: {
flexDirection: "row",
alignItems: "center",
flexDirection: 'row',
alignItems: 'center',
},
value: {
fontFamily: FONT_FAMILY.InterSemiBold,
fontSize: 16,
color: "#fff",
color: '#fff',
},
icon: {
width: 18,
height: 18,
},
});
})
export default CreditAmount;
export default CreditAmount
+6 -7
View File
@@ -1,8 +1,7 @@
import Dialog from "react-native-dialog";
export const Container = Dialog.Container;
export const Button = Dialog.Button;
export const Title = Dialog.Title;
export const Input = Dialog.Input;
export const Description = Dialog.Description;
import Dialog from 'react-native-dialog'
export const Container = Dialog.Container
export const Button = Dialog.Button
export const Title = Dialog.Title
export const Input = Dialog.Input
export const Description = Dialog.Description
+24 -29
View File
@@ -1,29 +1,29 @@
import React from "react";
import { Text, View } from "react-native";
import React from 'react'
import { Text, View } from 'react-native'
import { Input as MinuitInput } from "../Input";
import { BaseButton as MinuitButton } from "../Button";
import Overlay from "../Overlay";
import { Input as MinuitInput } from '../Input'
import { BaseButton as MinuitButton } from '../Button'
import Overlay from '../Overlay'
import { Fonts, Palette, gutters } from "../../styles";
import Style from "../../styles/Style";
import { Fonts, Palette, gutters } from '../../styles'
import Style from '../../styles/Style'
export const Container = ({ visible, setVisible = () => null, children }) => {
return (
<Overlay isVisible={visible}>
<View style={{ ...Style.containerModal }}>{children}</View>
</Overlay>
);
};
)
}
export const Title = ({ children }) => {
return (
<Text
style={{
...Fonts({
type: "title",
type: 'title',
style: {
textAlign: "center",
textAlign: 'center',
marginBottom: gutters,
},
}),
@@ -31,17 +31,17 @@ export const Title = ({ children }) => {
>
{children}
</Text>
);
};
)
}
export const Description = ({ children }) => {
return (
<Text
style={{
...Fonts({
type: "default",
type: 'default',
style: {
textAlign: "center",
textAlign: 'center',
marginBottom: gutters / 2,
},
}),
@@ -49,29 +49,24 @@ export const Description = ({ children }) => {
>
{children}
</Text>
);
};
)
}
export const Button = ({
label,
onPress,
type = "primary",
containerStyle = {},
}) => {
export const Button = ({ label, onPress, type = 'primary', containerStyle = {} }) => {
return (
<MinuitButton
containerStyle={{
width: "100%",
alignSelf: "center",
width: '100%',
alignSelf: 'center',
...containerStyle,
}}
text={label}
onPress={onPress}
type={type}
/>
);
};
)
}
export const Input = (props) => {
return <MinuitInput {...props} setValue={props.onChangeText} />;
};
return <MinuitInput {...props} setValue={props.onChangeText} />
}
+152 -154
View File
@@ -1,27 +1,27 @@
import { useActionSheet } from "@expo/react-native-action-sheet";
import * as DocumentPicker from "expo-document-picker";
import * as ImagePicker from "expo-image-picker";
import moment from "moment";
import { Image, Pressable, Text, View } from "react-native";
import Compressor from "react-native-compressor";
import useMinuit from "react-native-minuit/src/hooks/useMinuit";
import React, { useCallback, useEffect, useRef, useState } from "reactn";
import { useActionSheet } from '@expo/react-native-action-sheet'
import * as DocumentPicker from 'expo-document-picker'
import * as ImagePicker from 'expo-image-picker'
import moment from 'moment'
import { Image, Pressable, Text, View } from 'react-native'
import Compressor from 'react-native-compressor'
import useMinuit from 'react-native-minuit/src/hooks/useMinuit'
import React, { useCallback, useEffect, useRef, useState } from 'reactn'
import { arrayUnion } from "../config/firebase";
import { arrayUnion } from '../config/firebase'
import { icons } from "../assets";
import { Fonts, Palette } from "../styles";
import Style, { gutterConstant, mainBorderRadius } from "../styles/Style";
import { icons } from '../assets'
import { Fonts, Palette } from '../styles'
import Style, { gutterConstant, mainBorderRadius } from '../styles/Style'
import { uploadFileToFirebase } from "../helpers/uploadToFirebase";
import useLayoutType from "../hooks/useLayoutType";
import { uploadFileToFirebase } from '../helpers/uploadToFirebase'
import useLayoutType from '../hooks/useLayoutType'
const NATIVE_OPTIONS = [
"Importer depuis la galerie",
"Ajouter un fichier",
"Prendre une photo ou une vidéo",
"Annuler",
];
'Importer depuis la galerie',
'Ajouter un fichier',
'Prendre une photo ou une vidéo',
'Annuler',
]
const DocumentDropZone = ({
containerStyle = {},
@@ -35,162 +35,162 @@ const DocumentDropZone = ({
customElement = null,
shouldReturnObject = false,
}) => {
const dropRef = useRef(null);
const dropRef = useRef(null)
const [isDragging, setIsDragging] = useState(false);
const [isDragging, setIsDragging] = useState(false)
const { setIsLoading, setTooltip } = useMinuit();
const { isDesktop, isWeb, isNative } = useLayoutType();
const { showActionSheetWithOptions } = useActionSheet();
const { setIsLoading, setTooltip } = useMinuit()
const { isDesktop, isWeb, isNative } = useLayoutType()
const { showActionSheetWithOptions } = useActionSheet()
useEffect(() => {
if (isWeb && dropRef.current) {
const el = dropRef.current;
const el = dropRef.current
const handleDragIn = (e) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
};
e.preventDefault()
e.stopPropagation()
setIsDragging(true)
}
const handleDragOut = (e) => {
e.preventDefault();
e.stopPropagation();
e.preventDefault()
e.stopPropagation()
if (!dropRef.current.contains(e.relatedTarget)) {
console.log("drag left");
setIsDragging(false);
console.log('drag left')
setIsDragging(false)
}
};
}
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
e.preventDefault()
e.stopPropagation()
setIsDragging(false)
let files = [...e.dataTransfer.files];
let files = [...e.dataTransfer.files]
if (files.length > 0) {
for (const file of files) {
handleWebFile({ file });
handleWebFile({ file })
}
}
};
}
el.addEventListener("dragenter", handleDragIn);
el.addEventListener("dragleave", handleDragOut);
el.addEventListener("dragover", (e) => e.preventDefault());
el.addEventListener("drop", handleDrop);
el.addEventListener('dragenter', handleDragIn)
el.addEventListener('dragleave', handleDragOut)
el.addEventListener('dragover', (e) => e.preventDefault())
el.addEventListener('drop', handleDrop)
return () => {
el.removeEventListener("dragenter", handleDragIn);
el.removeEventListener("dragleave", handleDragOut);
el.removeEventListener("dragover", (e) => e.preventDefault());
el.removeEventListener("drop", handleDrop);
};
el.removeEventListener('dragenter', handleDragIn)
el.removeEventListener('dragleave', handleDragOut)
el.removeEventListener('dragover', (e) => e.preventDefault())
el.removeEventListener('drop', handleDrop)
}
}
}, [dropRef?.current]);
}, [dropRef?.current])
const handleWebFile = ({ file }) => {
try {
const { type = "" } = file;
const reader = new FileReader();
const { type = '' } = file
const reader = new FileReader()
reader.onloadend = () => {
const base64 = reader.result.split(",")[1]; // Vous pourriez avoir besoin de cette valeur base64 pour un upload direct
const uri = reader.result; // URI en base64 du fichier
const base64 = reader.result.split(',')[1] // Vous pourriez avoir besoin de cette valeur base64 pour un upload direct
const uri = reader.result // URI en base64 du fichier
console.log("file", file);
console.log('file', file)
onUploadDocument({ files: [{ name: file.name, uri, type }] });
};
onUploadDocument({ files: [{ name: file.name, uri, type }] })
}
reader.onerror = (err) => {
console.error("FileReader error", err);
};
console.error('FileReader error', err)
}
reader.readAsDataURL(file); // Lire le fichier et déclencher reader.onloadend lorsque c'est fait
reader.readAsDataURL(file) // Lire le fichier et déclencher reader.onloadend lorsque c'est fait
} catch (error) {
console.log(error);
setTooltip({ text: error.message, type: "error" });
console.log(error)
setTooltip({ text: error.message, type: 'error' })
}
};
}
const onAddFile = async ({} = {}) => {
try {
if (isNative) {
const cancelButtonIndex = NATIVE_OPTIONS.length - 1;
const cancelButtonIndex = NATIVE_OPTIONS.length - 1
showActionSheetWithOptions(
{
options: NATIVE_OPTIONS,
cancelButtonIndex,
userInterfaceStyle: "dark",
userInterfaceStyle: 'dark',
...Style.actionSheet,
},
async (selectedIndex) => {
if (selectedIndex !== cancelButtonIndex) {
switch (selectedIndex) {
case 0:
onChooseLibrary();
break;
onChooseLibrary()
break
case 1:
onChooseDocumentPicker();
break;
onChooseDocumentPicker()
break
case 2:
onTakePicture();
break;
onTakePicture()
break
default:
break;
break
}
}
}
);
)
} else {
onChooseDocumentPicker();
onChooseDocumentPicker()
}
} catch (error) {
console.log(error);
setTooltip({ text: error.message, type: "error" });
console.log(error)
setTooltip({ text: error.message, type: 'error' })
} finally {
setIsLoading(false);
setIsLoading(false)
}
};
}
const onChooseLibrary = async ({} = {}) => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsEditing: false,
quality: 3,
});
})
if (result?.assets?.[0]?.uri) {
const { uri, fileName = "" } = result?.assets?.[0];
const { uri, fileName = '' } = result?.assets?.[0]
onUploadDocument({
files: [
{
name: getAssetName({ fileName, uri }),
uri,
type: "IMAGE",
type: 'IMAGE',
},
],
});
})
} else {
throw new Error("Aucune image sélectionnée");
throw new Error('Aucune image sélectionnée')
}
};
}
const onChooseDocumentPicker = async ({} = {}) => {
const result = await DocumentPicker.getDocumentAsync({
type: "*/*",
type: '*/*',
copyToCacheDirectory: false,
});
})
if (result?.assets?.length > 0) {
const { name = "", uri = null } = result?.assets[0] || {};
const { name = '', uri = null } = result?.assets[0] || {}
if (!uri) {
throw new Error("Erreur lors de l'ajout du document");
throw new Error("Erreur lors de l'ajout du document")
}
onUploadDocument({
@@ -200,117 +200,117 @@ const DocumentDropZone = ({
uri,
},
],
});
})
} else {
console.log(result);
throw new Error("Erreur lors de l'ajout du document");
console.log(result)
throw new Error("Erreur lors de l'ajout du document")
}
};
}
const onTakePicture = useCallback(async () => {
const { status } = await ImagePicker.requestCameraPermissionsAsync();
const { status } = await ImagePicker.requestCameraPermissionsAsync()
if (status !== "granted") {
throw new Error("Permissions caméra non accordées");
if (status !== 'granted') {
throw new Error('Permissions caméra non accordées')
}
const result = await ImagePicker.launchCameraAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsEditing: true,
quality: 1,
});
})
if (result?.assets?.[0]?.uri) {
const { uri, fileName = "" } = result?.assets?.[0];
const { uri, fileName = '' } = result?.assets?.[0]
onUploadDocument({
files: [
{
name: getAssetName({ fileName, uri }),
uri,
type: "IMAGE",
type: 'IMAGE',
},
],
});
})
} else {
throw new Error("Aucune image sélectionnée");
throw new Error('Aucune image sélectionnée')
}
}, []);
}, [])
const getDefaultFileName = () => {
const randomID = Math.random().toString(36).substring(7);
return `${moment().format(`DD_MM_YYYY_HH_mm_ss`)}_${randomID}`;
};
const randomID = Math.random().toString(36).substring(7)
return `${moment().format(`DD_MM_YYYY_HH_mm_ss`)}_${randomID}`
}
const getAssetName = ({ fileName = "", uri = "" }) => {
let name = fileName;
const getAssetName = ({ fileName = '', uri = '' }) => {
let name = fileName
if (!name && uri?.startsWith("file://")) {
const splittedArray = uri?.split("/") || [];
name = splittedArray?.[splittedArray?.length - 1] || "";
if (!name && uri?.startsWith('file://')) {
const splittedArray = uri?.split('/') || []
name = splittedArray?.[splittedArray?.length - 1] || ''
}
if (!name?.length) {
let extension = uri?.split(";")?.[0]?.split("/")?.[1] || "";
let extension = uri?.split(';')?.[0]?.split('/')?.[1] || ''
const defaultFileName = getDefaultFileName();
const defaultFileName = getDefaultFileName()
if (extension?.length) {
name = `${defaultFileName}.${extension}`;
name = `${defaultFileName}.${extension}`
} else {
name = `${defaultFileName}`;
name = `${defaultFileName}`
}
}
return name;
};
return name
}
const onUploadDocument = async ({ files = [] } = {}) => {
try {
setIsLoading(true);
setIsLoading(true)
const filesToUpdate = [];
const filesToUpdate = []
await Promise.all(
files.map(async (file) => {
const { name = "", uri = null } = file;
const { name = '', uri = null } = file
console.log(file);
console.log(file)
if (!uri) {
throw new Error("Erreur lors de l'ajout du document");
throw new Error("Erreur lors de l'ajout du document")
}
const fileName = name || getDefaultFileName();
let type = "FILE";
const fileName = name || getDefaultFileName()
let type = 'FILE'
if (name?.toLowerCase()?.match(/\.(jpeg|jpg|gif|png)$/) != null) {
type = "IMAGE";
type = 'IMAGE'
}
if (name?.toLowerCase()?.match(/\.(mp4|mov|avi|mkv)$/) != null) {
type = "VIDEO";
type = 'VIDEO'
}
let compressedURI = uri;
let thumbnailURI = null;
let compressedURI = uri
let thumbnailURI = null
if (isNative) {
if (type === "IMAGE") {
if (type === 'IMAGE') {
compressedURI = await Compressor.Image.compress(uri, {
compressionMethod: "manual",
compressionMethod: 'manual',
maxWidth: 1000,
quality: 0.8,
});
} else if (type === "VIDEO") {
compressedURI = await Compressor.Video.compress(uri);
})
} else if (type === 'VIDEO') {
compressedURI = await Compressor.Video.compress(uri)
}
}
const { resultURI = null } = await uploadFileToFirebase({
uri: compressedURI,
path: `documents/${documentID}/files/${fileName}`,
});
})
if (resultURI) {
if (shouldReturnObject) {
@@ -319,39 +319,39 @@ const DocumentDropZone = ({
uri: resultURI,
type,
thumbnailURI,
});
})
} else {
filesToUpdate.push(resultURI);
filesToUpdate.push(resultURI)
}
} else {
console.log("resultURI is null");
console.log('resultURI is null')
}
})
);
)
if (filesToUpdate.length > 0) {
if (documentExists) {
await collectionRef.doc(documentID).update({
files: arrayUnion(...filesToUpdate),
});
})
}
if (setFiles) {
setFiles((prev) => [...prev, ...filesToUpdate]);
setFiles((prev) => [...prev, ...filesToUpdate])
}
}
setTooltip({ text: "Document(s) ajouté(s) avec succès" });
setTooltip({ text: 'Document(s) ajouté(s) avec succès' })
} catch (error) {
console.log(error);
setTooltip({ text: error.message, type: "error" });
console.log(error)
setTooltip({ text: error.message, type: 'error' })
} finally {
setIsLoading(false);
setIsLoading(false)
}
};
}
if (!documentID) {
return null;
return null
}
return (
@@ -361,11 +361,9 @@ const DocumentDropZone = ({
<View
style={{
...Style.containerCenter,
backgroundColor: isDragging
? Palette.transparentGreen
: Palette.transparentPrimary,
backgroundColor: isDragging ? Palette.transparentGreen : Palette.transparentPrimary,
borderRadius: mainBorderRadius,
borderStyle: "dashed",
borderStyle: 'dashed',
borderWidth: 2,
borderColor: isDragging ? Palette.green : Palette.primary,
...containerStyle,
@@ -380,20 +378,20 @@ const DocumentDropZone = ({
/>
<Text
style={{
...Fonts({ type: "default" }),
textAlign: "center",
...Fonts({ type: 'default' }),
textAlign: 'center',
color: Palette.white,
}}
>
{isWeb && isDesktop
? `Glissez-déposez ici\nles fichiers, images et vidéos\nà ajouter.`
: "Ajouter une image,\nun fichier ou une vidéo."}
: 'Ajouter une image,\nun fichier ou une vidéo.'}
</Text>
</View>
)}
</Pressable>
</View>
);
};
)
}
export default DocumentDropZone;
export default DocumentDropZone
+16 -22
View File
@@ -1,40 +1,34 @@
import React, { useState, useEffect } from "react";
import { View } from "react-native";
import { Image } from "expo-image";
import React, { useState, useEffect } from 'react'
import { View } from 'react-native'
import { Image } from 'expo-image'
const DynamicImage = ({ uri, children, ...props }) => {
const [loading, setLoading] = useState(true);
const [numRetries, setNumRetries] = useState(0);
const [loading, setLoading] = useState(true)
const [numRetries, setNumRetries] = useState(0)
useEffect(() => {
if (numRetries < 15) {
const timer = setTimeout(() => {
setLoading(true);
}, 2500);
return () => clearTimeout(timer);
setLoading(true)
}, 2500)
return () => clearTimeout(timer)
}
}, [numRetries]);
}, [numRetries])
const handleError = () => {
setLoading(false);
setNumRetries(numRetries + 1);
};
setLoading(false)
setNumRetries(numRetries + 1)
}
return (
<View style={{ flex: 1 }}>
{loading && numRetries < 15 && uri ? (
<Image
source={uri}
contentFit="cover"
transition={500}
{...props}
onError={handleError}
/>
<Image source={uri} contentFit="cover" transition={500} {...props} onError={handleError} />
) : (
<View {...props}>{children}</View>
)}
</View>
);
};
)
}
export default DynamicImage;
export default DynamicImage
+22 -28
View File
@@ -1,60 +1,54 @@
import React, { useGlobal } from "reactn";
import { View, Text } from "react-native";
import React, { useGlobal } from 'reactn'
import { View, Text } from 'react-native'
import useLayoutType from "../hooks/useLayoutType";
import useLayoutType from '../hooks/useLayoutType'
import Button from "./Button";
import Button from './Button'
import { Style, Fonts } from "../styles";
import { responsiveScreenHeight } from "react-native-responsive-dimensions";
import { Style, Fonts } from '../styles'
import { responsiveScreenHeight } from 'react-native-responsive-dimensions'
const EmptyFlashListPlaceholder = ({
loading = false,
text = "-",
buttonData = {},
}) => {
const [, setShowSearch] = useGlobal("showSearch");
const EmptyFlashListPlaceholder = ({ loading = false, text = '-', buttonData = {} }) => {
const [, setShowSearch] = useGlobal('showSearch')
const { isMobile } = useLayoutType;
const { isMobile } = useLayoutType
if (loading) {
return (
<Text style={Fonts({ type: "default", style: { textAlign: "center" } })}>
<Text style={Fonts({ type: 'default', style: { textAlign: 'center' } })}>
Chargement en cours...
</Text>
);
)
}
return (
<View
style={{
...Style.containerCenter,
alignSelf: "center",
width: isMobile ? "100%" : "50%",
alignSelf: 'center',
width: isMobile ? '100%' : '50%',
height: responsiveScreenHeight(50),
}}
>
<Text style={Fonts({ type: "default", style: { textAlign: "center" } })}>
{text}
</Text>
<Text style={Fonts({ type: 'default', style: { textAlign: 'center' } })}>{text}</Text>
{buttonData?.text?.length > 0 && (
<Button
text={buttonData?.text || "Ajouter une tâche"}
text={buttonData?.text || 'Ajouter une tâche'}
type="secondary"
onPress={() => {
setShowSearch(false);
setShowSearch(false)
buttonData?.onPress?.() || (() => console.log("null"));
buttonData?.onPress?.() || (() => console.log('null'))
}}
containerStyle={{
alignSelf: "center",
width: "100%",
alignSelf: 'center',
width: '100%',
}}
/>
)}
</View>
);
};
)
}
export default EmptyFlashListPlaceholder;
export default EmptyFlashListPlaceholder
+158 -188
View File
@@ -1,54 +1,41 @@
import { BlurView } from "expo-blur";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
Animated,
FlatList,
Platform,
StyleSheet,
View,
useWindowDimensions,
} from "react-native";
import { responsiveHeight } from "../../actions/responsiveSizes";
import { ai, cardsImg } from "../../assets";
import { gutters } from "../../styles";
import { getCreationStageStates } from "../../utils/projectStages";
import AnimatedPaginationDot from "../AnimatedPaginationDot/AnimatedPaginationDot";
import PersonaCard from "../cards/PersonaCard/PersonaCard";
import { BlurView } from 'expo-blur'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Animated, FlatList, Platform, StyleSheet, View, useWindowDimensions } from 'react-native'
import { responsiveHeight } from '../../actions/responsiveSizes'
import { ai, cardsImg } from '../../assets'
import { gutters } from '../../styles'
import { getCreationStageStates } from '../../utils/projectStages'
import AnimatedPaginationDot from '../AnimatedPaginationDot/AnimatedPaginationDot'
import PersonaCard from '../cards/PersonaCard/PersonaCard'
const STAGE_CARD_CONTENT = [
{
key: "songwriter",
title: "Céline",
description: "Lets write lyrics together !",
key: 'songwriter',
title: 'Céline',
description: 'Lets write lyrics together !',
image: ai.leftIcon,
},
{
key: "beatmaker",
title: "Theo",
key: 'beatmaker',
title: 'Theo',
description: "Come back, when you'll have lyrics!",
image: ai.rightIcon,
},
{
key: "director",
title: "Theo",
key: 'director',
title: 'Theo',
description: "Theo t'accompagne pour créer ton playback.",
image: ai.rightIcon,
},
{
key: "publisher",
title: "Publication",
description: "Ta vidéo est prête ? Direction YouTube !",
key: 'publisher',
title: 'Publication',
description: 'Ta vidéo est prête ? Direction YouTube !',
image: cardsImg.production,
},
];
]
const WEB_SCROLL_INACTIVE_DELTA = 0.05;
const WEB_SCROLL_INACTIVE_DELTA = 0.05
const FeatureCarousel = ({
style,
@@ -59,231 +46,225 @@ const FeatureCarousel = ({
}) => {
const stageStates = useMemo(() => {
if (stageStatesProp) {
return stageStatesProp;
return stageStatesProp
}
return getCreationStageStates(selectedProject);
}, [stageStatesProp, selectedProject]);
return getCreationStageStates(selectedProject)
}, [stageStatesProp, selectedProject])
const stageStatesByKey = useMemo(() => {
if (!Array.isArray(stageStates)) {
return {};
return {}
}
return stageStates.reduce((acc, stage) => {
if (stage?.key) {
acc[stage.key] = stage;
acc[stage.key] = stage
}
return acc;
}, {});
}, [stageStates]);
return acc
}, {})
}, [stageStates])
const carouselItems = useMemo(
() =>
STAGE_CARD_CONTENT.map((item) => {
const state = stageStatesByKey[item.key];
const state = stageStatesByKey[item.key]
return {
...item,
isLocked: state?.isLocked ?? true,
description: state?.description ?? item.description,
};
}
}),
[stageStatesByKey]
);
)
const { height: windowHeight } = useWindowDimensions();
const isWeb = Platform.OS === "web";
const { height: windowHeight } = useWindowDimensions()
const isWeb = Platform.OS === 'web'
const [viewportHeight, setViewportHeight] = useState(() =>
Math.max(windowHeight, 1)
);
const [viewportHeight, setViewportHeight] = useState(() => Math.max(windowHeight, 1))
const updateSnapHeight = useCallback((height) => {
if (!height || Number.isNaN(height)) {
return;
return
}
setViewportHeight((prev) => {
if (prev == null || Math.abs(prev - height) > 0.5) {
return height;
return height
}
return prev;
});
}, []);
return prev
})
}, [])
useEffect(() => {
updateSnapHeight(Math.max(windowHeight, 1));
}, [updateSnapHeight, windowHeight]);
updateSnapHeight(Math.max(windowHeight, 1))
}, [updateSnapHeight, windowHeight])
const itemHeight = Math.max(viewportHeight, 1);
const itemHeight = Math.max(viewportHeight, 1)
const listRef = useRef(null);
const pendingScrollRef = useRef(false);
const alignTimeoutRef = useRef(null);
const activeIndexRef = useRef(
typeof activeIndex === "number" ? activeIndex : 0
);
const onActiveIndexChangeRef = useRef(onActiveIndexChange);
const scrollY = useRef(new Animated.Value(0)).current;
const listRef = useRef(null)
const pendingScrollRef = useRef(false)
const alignTimeoutRef = useRef(null)
const activeIndexRef = useRef(typeof activeIndex === 'number' ? activeIndex : 0)
const onActiveIndexChangeRef = useRef(onActiveIndexChange)
const scrollY = useRef(new Animated.Value(0)).current
useEffect(() => {
onActiveIndexChangeRef.current = onActiveIndexChange;
}, [onActiveIndexChange]);
onActiveIndexChangeRef.current = onActiveIndexChange
}, [onActiveIndexChange])
useEffect(() => {
if (typeof activeIndex === "number") {
activeIndexRef.current = activeIndex;
if (typeof activeIndex === 'number') {
activeIndexRef.current = activeIndex
}
}, [activeIndex]);
}, [activeIndex])
const clampIndex = useCallback(
(index) => {
if (!carouselItems.length) {
return 0;
return 0
}
if (index < 0) {
return 0;
return 0
}
if (index >= carouselItems.length) {
return carouselItems.length - 1;
return carouselItems.length - 1
}
return index;
return index
},
[carouselItems.length]
);
)
const clearPendingAlignment = useCallback(() => {
if (alignTimeoutRef.current != null) {
globalThis.clearTimeout(alignTimeoutRef.current);
alignTimeoutRef.current = null;
globalThis.clearTimeout(alignTimeoutRef.current)
alignTimeoutRef.current = null
}
}, []);
}, [])
useEffect(() => () => clearPendingAlignment(), [clearPendingAlignment]);
useEffect(() => () => clearPendingAlignment(), [clearPendingAlignment])
const scrollToIndex = useCallback(
(index, animated = true, heightOverride) => {
const ref = listRef.current;
const ref = listRef.current
if (!ref) {
return;
return
}
const clamped = clampIndex(index);
const height =
heightOverride && heightOverride > 0 ? heightOverride : itemHeight;
const clamped = clampIndex(index)
const height = heightOverride && heightOverride > 0 ? heightOverride : itemHeight
if (isWeb) {
if (!height) {
return;
return
}
updateSnapHeight(height);
updateSnapHeight(height)
try {
ref.scrollToOffset({ offset: clamped * height, animated });
ref.scrollToOffset({ offset: clamped * height, animated })
} catch (_error) {
// Ignore scroll errors when list is not ready yet.
}
activeIndexRef.current = clamped;
return;
activeIndexRef.current = clamped
return
}
try {
pendingScrollRef.current = !!animated;
ref.scrollToIndex({ index: clamped, animated });
activeIndexRef.current = clamped;
pendingScrollRef.current = !!animated
ref.scrollToIndex({ index: clamped, animated })
activeIndexRef.current = clamped
} catch (_error) {
pendingScrollRef.current = false;
pendingScrollRef.current = false
}
},
[clampIndex, isWeb, itemHeight, updateSnapHeight]
);
)
useEffect(() => {
if (
listRef.current == null ||
typeof activeIndex !== "number" ||
typeof activeIndex !== 'number' ||
activeIndex < 0 ||
activeIndex >= carouselItems.length ||
(isWeb && !itemHeight)
) {
return;
return
}
scrollToIndex(activeIndex);
}, [activeIndex, carouselItems.length, isWeb, itemHeight, scrollToIndex]);
scrollToIndex(activeIndex)
}, [activeIndex, carouselItems.length, isWeb, itemHeight, scrollToIndex])
useEffect(() => {
if (listRef.current == null || (isWeb && !itemHeight)) {
return;
return
}
scrollToIndex(activeIndexRef.current, false);
}, [carouselItems.length, isWeb, itemHeight, scrollToIndex]);
scrollToIndex(activeIndexRef.current, false)
}, [carouselItems.length, isWeb, itemHeight, scrollToIndex])
const alignToOffset = useCallback(
(offset, layoutHeight) => {
const height =
layoutHeight && layoutHeight > 0 ? layoutHeight : itemHeight;
const height = layoutHeight && layoutHeight > 0 ? layoutHeight : itemHeight
if (!height) {
return;
return
}
updateSnapHeight(height);
updateSnapHeight(height)
const currentIndex = activeIndexRef.current;
const rawIndex = height ? offset / height : currentIndex;
const currentIndex = activeIndexRef.current
const rawIndex = height ? offset / height : currentIndex
let nextIndex = currentIndex;
let nextIndex = currentIndex
if (isWeb) {
const delta = rawIndex - currentIndex;
const delta = rawIndex - currentIndex
if (Math.abs(delta) > WEB_SCROLL_INACTIVE_DELTA) {
if (Math.abs(delta) <= 1) {
nextIndex = clampIndex(currentIndex + (delta > 0 ? 1 : -1));
nextIndex = clampIndex(currentIndex + (delta > 0 ? 1 : -1))
} else {
nextIndex = clampIndex(currentIndex + Math.round(delta));
nextIndex = clampIndex(currentIndex + Math.round(delta))
}
}
} else {
nextIndex = clampIndex(Math.round(rawIndex));
nextIndex = clampIndex(Math.round(rawIndex))
}
const hasChanged = nextIndex !== activeIndexRef.current;
const hasChanged = nextIndex !== activeIndexRef.current
if (hasChanged) {
activeIndexRef.current = nextIndex;
const callback = onActiveIndexChangeRef.current;
activeIndexRef.current = nextIndex
const callback = onActiveIndexChangeRef.current
if (callback) {
callback(nextIndex);
callback(nextIndex)
}
}
if (isWeb || hasChanged) {
const shouldAnimate = isWeb ? true : !isWeb;
scrollToIndex(nextIndex, shouldAnimate, height);
const shouldAnimate = isWeb ? true : !isWeb
scrollToIndex(nextIndex, shouldAnimate, height)
}
},
[clampIndex, isWeb, itemHeight, scrollToIndex, updateSnapHeight]
);
)
const handleScrollEnd = useCallback(
(event) => {
clearPendingAlignment();
const offsetY = event?.nativeEvent?.contentOffset?.y ?? 0;
const layoutHeight = event?.nativeEvent?.layoutMeasurement?.height;
alignToOffset(offsetY, layoutHeight);
clearPendingAlignment()
const offsetY = event?.nativeEvent?.contentOffset?.y ?? 0
const layoutHeight = event?.nativeEvent?.layoutMeasurement?.height
alignToOffset(offsetY, layoutHeight)
},
[alignToOffset, clearPendingAlignment]
);
)
const handleScroll = useCallback(
(event) => {
if (!isWeb) {
return;
return
}
const offsetY = event?.nativeEvent?.contentOffset?.y ?? 0;
const layoutHeight = event?.nativeEvent?.layoutMeasurement?.height;
clearPendingAlignment();
const offsetY = event?.nativeEvent?.contentOffset?.y ?? 0
const layoutHeight = event?.nativeEvent?.layoutMeasurement?.height
clearPendingAlignment()
alignTimeoutRef.current = globalThis.setTimeout(() => {
alignToOffset(offsetY, layoutHeight);
alignTimeoutRef.current = null;
}, 80);
alignToOffset(offsetY, layoutHeight)
alignTimeoutRef.current = null
}, 80)
},
[alignToOffset, clearPendingAlignment, isWeb]
);
)
const animatedScrollHandler = useMemo(
() =>
@@ -292,31 +273,26 @@ const FeatureCarousel = ({
listener: isWeb ? handleScroll : undefined,
}),
[handleScroll, isWeb, scrollY]
);
)
const handleLayout = useCallback(
(event) => {
const layoutHeight = event?.nativeEvent?.layout?.height ?? 0;
const layoutHeight = event?.nativeEvent?.layout?.height ?? 0
if (layoutHeight > 0) {
updateSnapHeight(layoutHeight);
updateSnapHeight(layoutHeight)
}
},
[updateSnapHeight]
);
)
const keyExtractor = useCallback((item) => item.key, []);
const keyExtractor = useCallback((item) => item.key, [])
const renderItem = useCallback(
({ item, index }) => (
<PersonaCard
item={item}
index={index}
isLock={item.isLocked}
height={itemHeight}
/>
<PersonaCard item={item} index={index} isLock={item.isLocked} height={itemHeight} />
),
[itemHeight]
);
)
const getItemLayout = useCallback(
(_data, index) => ({
@@ -325,50 +301,44 @@ const FeatureCarousel = ({
index,
}),
[itemHeight]
);
)
const viewabilityConfig = useRef({ viewAreaCoveragePercentThreshold: 60 });
const viewabilityConfig = useRef({ viewAreaCoveragePercentThreshold: 60 })
const handleViewableItemsChangedRef = useRef();
const handleViewableItemsChangedRef = useRef()
if (!handleViewableItemsChangedRef.current) {
handleViewableItemsChangedRef.current = ({ viewableItems }) => {
if (!viewableItems?.length) return;
const firstVisible = viewableItems.find((item) => item?.isViewable);
if (!firstVisible || firstVisible.index == null) return;
if (!viewableItems?.length) return
const firstVisible = viewableItems.find((item) => item?.isViewable)
if (!firstVisible || firstVisible.index == null) return
if (pendingScrollRef.current) {
if (firstVisible.index === activeIndexRef.current) {
pendingScrollRef.current = false;
pendingScrollRef.current = false
}
return;
return
}
const callback = onActiveIndexChangeRef.current;
const callback = onActiveIndexChangeRef.current
if (callback && firstVisible.index !== activeIndexRef.current) {
callback(firstVisible.index);
callback(firstVisible.index)
}
};
}
}
const snapOffsets = useMemo(() => {
if (!itemHeight || !isWeb) {
return undefined;
return undefined
}
return carouselItems.map((_, index) => index * itemHeight);
}, [carouselItems, isWeb, itemHeight]);
return carouselItems.map((_, index) => index * itemHeight)
}, [carouselItems, isWeb, itemHeight])
const blurIntensity = isWeb ? 80 : 30;
const blurIntensity = isWeb ? 80 : 30
const dotsWrapperStyle = useMemo(
() => [
styles.dotsWrapperBase,
isWeb ? styles.dotsWrapperWeb : styles.dotsWrapperNative,
],
() => [styles.dotsWrapperBase, isWeb ? styles.dotsWrapperWeb : styles.dotsWrapperNative],
[isWeb]
);
)
return (
<View
style={[styles.container, isWeb && styles.containerWeb, style]}
onLayout={handleLayout}
>
<View style={[styles.container, isWeb && styles.containerWeb, style]} onLayout={handleLayout}>
<FlatList
ref={listRef}
data={carouselItems}
@@ -385,11 +355,11 @@ const FeatureCarousel = ({
maxToRenderPerBatch={2}
windowSize={3}
scrollEventThrottle={16}
snapToAlignment={isWeb ? undefined : "start"}
snapToAlignment={isWeb ? undefined : 'start'}
snapToInterval={!isWeb && itemHeight ? itemHeight : undefined}
snapToOffsets={snapOffsets}
disableIntervalMomentum={!isWeb}
decelerationRate={!isWeb ? "fast" : undefined}
decelerationRate={!isWeb ? 'fast' : undefined}
style={styles.list}
onScroll={animatedScrollHandler}
onMomentumScrollEnd={handleScrollEnd}
@@ -397,7 +367,7 @@ const FeatureCarousel = ({
/>
<BlurView
intensity={blurIntensity}
tint={Platform.OS === "web" ? undefined : "dark"}
tint={Platform.OS === 'web' ? undefined : 'dark'}
style={dotsWrapperStyle}
pointerEvents="none"
>
@@ -414,16 +384,16 @@ const FeatureCarousel = ({
/>
</BlurView>
</View>
);
};
)
}
export default FeatureCarousel;
export default FeatureCarousel
const styles = StyleSheet.create({
container: {
flex: 1,
width: "100%",
flexDirection: "row",
width: '100%',
flexDirection: 'row',
paddingBottom: responsiveHeight(5),
paddingHorizontal: gutters,
},
@@ -434,28 +404,28 @@ const styles = StyleSheet.create({
flex: 1,
},
dotsWrapperBase: {
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
borderRadius: 16,
paddingVertical: 12,
paddingHorizontal: 8,
overflow: "hidden",
backgroundColor: "rgba(18, 18, 18, 0.2)",
overflow: 'hidden',
backgroundColor: 'rgba(18, 18, 18, 0.2)',
},
dotsWrapperWeb: {
position: "absolute",
position: 'absolute',
right: 12,
top: 0,
bottom: 0,
maxHeight: "75%",
alignSelf: "center",
maxHeight: '75%',
alignSelf: 'center',
},
dotsWrapperNative: {
marginLeft: 12,
alignSelf: "center",
alignSelf: 'center',
},
dotsContainer: {
flexDirection: "column",
flexDirection: 'column',
},
dot: {
width: 8,
@@ -463,6 +433,6 @@ const styles = StyleSheet.create({
marginHorizontal: 0,
marginVertical: 6,
borderRadius: 999,
backgroundColor: "rgba(255,255,255,0.4)",
backgroundColor: 'rgba(255,255,255,0.4)',
},
});
})
@@ -1,11 +1,5 @@
import { BlurView } from "expo-blur";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { BlurView } from 'expo-blur'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
Animated,
ImageBackground,
@@ -13,42 +7,42 @@ import {
StyleSheet,
View,
useWindowDimensions,
} from "react-native";
import { ai, cardsImg } from "../../assets";
import { getCreationStageStates } from "../../utils/projectStages";
import AnimatedPaginationDot from "../AnimatedPaginationDot/AnimatedPaginationDot";
import PersonaCard from "../cards/PersonaCard/PersonaCard";
import { LinearGradient } from "../LinearGradient/LinearGradient";
} from 'react-native'
import { ai, cardsImg } from '../../assets'
import { getCreationStageStates } from '../../utils/projectStages'
import AnimatedPaginationDot from '../AnimatedPaginationDot/AnimatedPaginationDot'
import PersonaCard from '../cards/PersonaCard/PersonaCard'
import { LinearGradient } from '../LinearGradient/LinearGradient'
const STAGE_CARD_CONTENT = [
{
key: "songwriter",
title: "Céline",
description: "Lets write lyrics together !",
key: 'songwriter',
title: 'Céline',
description: 'Lets write lyrics together !',
image: ai.leftIcon,
},
{
key: "beatmaker",
title: "Theo",
key: 'beatmaker',
title: 'Theo',
description: "Come back, when you'll have lyrics!",
image: ai.rightIcon,
},
{
key: "director",
title: "Theo",
key: 'director',
title: 'Theo',
description: "Theo t'accompagne pour créer ton playback.",
image: ai.rightIcon,
},
{
key: "publisher",
title: "Publication",
description: "Ta vidéo est prête ? Direction YouTube !",
key: 'publisher',
title: 'Publication',
description: 'Ta vidéo est prête ? Direction YouTube !',
image: cardsImg.production,
},
];
]
const WEB_SCROLL_INACTIVE_DELTA = 0.05;
const HOME_BACKGROUND_COLOR = "#425B87"; // Derived from the bottom color of the home hero image
const WEB_SCROLL_INACTIVE_DELTA = 0.05
const HOME_BACKGROUND_COLOR = '#425B87' // Derived from the bottom color of the home hero image
const FeatureCarousel = ({
style,
selectedProject,
@@ -60,257 +54,245 @@ const FeatureCarousel = ({
}) => {
const stageStates = useMemo(() => {
if (stageStatesProp) {
return stageStatesProp;
return stageStatesProp
}
return getCreationStageStates(selectedProject);
}, [stageStatesProp, selectedProject]);
return getCreationStageStates(selectedProject)
}, [stageStatesProp, selectedProject])
const stageStatesByKey = useMemo(() => {
if (!Array.isArray(stageStates)) {
return {};
return {}
}
return stageStates.reduce((acc, stage) => {
if (stage?.key) {
acc[stage.key] = stage;
acc[stage.key] = stage
}
return acc;
}, {});
}, [stageStates]);
return acc
}, {})
}, [stageStates])
const carouselItems = useMemo(
() =>
STAGE_CARD_CONTENT.map((item) => {
const state = stageStatesByKey[item.key];
const state = stageStatesByKey[item.key]
return {
...item,
isLocked: state?.isLocked ?? true,
description: state?.description ?? item.description,
};
}
}),
[stageStatesByKey]
);
)
const { height: windowHeight } = useWindowDimensions();
const isWeb = Platform.OS === "web";
const { height: windowHeight } = useWindowDimensions()
const isWeb = Platform.OS === 'web'
const [viewportHeight, setViewportHeight] = useState(() =>
Math.max(windowHeight, 1)
);
const [viewportHeight, setViewportHeight] = useState(() => Math.max(windowHeight, 1))
const updateSnapHeight = useCallback((height) => {
if (!height || Number.isNaN(height)) {
return;
return
}
setViewportHeight((prev) => {
if (prev == null || Math.abs(prev - height) > 0.5) {
return height;
return height
}
return prev;
});
}, []);
return prev
})
}, [])
useEffect(() => {
updateSnapHeight(Math.max(windowHeight, 1));
}, [updateSnapHeight, windowHeight]);
updateSnapHeight(Math.max(windowHeight, 1))
}, [updateSnapHeight, windowHeight])
const itemHeight = Math.max(viewportHeight, 1);
const itemHeight = Math.max(viewportHeight, 1)
const scrollViewRef = useRef(null);
const alignTimeoutRef = useRef(null);
const activeIndexRef = useRef(
typeof activeIndex === "number" ? activeIndex : 0
);
const onActiveIndexChangeRef = useRef(onActiveIndexChange);
const scrollY = useRef(new Animated.Value(0)).current;
const scrollViewRef = useRef(null)
const alignTimeoutRef = useRef(null)
const activeIndexRef = useRef(typeof activeIndex === 'number' ? activeIndex : 0)
const onActiveIndexChangeRef = useRef(onActiveIndexChange)
const scrollY = useRef(new Animated.Value(0)).current
useEffect(() => {
onActiveIndexChangeRef.current = onActiveIndexChange;
}, [onActiveIndexChange]);
onActiveIndexChangeRef.current = onActiveIndexChange
}, [onActiveIndexChange])
useEffect(() => {
if (typeof activeIndex === "number") {
activeIndexRef.current = activeIndex;
if (typeof activeIndex === 'number') {
activeIndexRef.current = activeIndex
}
}, [activeIndex]);
}, [activeIndex])
const clampIndex = useCallback(
(index) => {
if (!carouselItems.length) {
return 0;
return 0
}
if (index < 0) {
return 0;
return 0
}
if (index >= carouselItems.length) {
return carouselItems.length - 1;
return carouselItems.length - 1
}
return index;
return index
},
[carouselItems.length]
);
)
const clearPendingAlignment = useCallback(() => {
if (alignTimeoutRef.current != null) {
globalThis.clearTimeout(alignTimeoutRef.current);
alignTimeoutRef.current = null;
globalThis.clearTimeout(alignTimeoutRef.current)
alignTimeoutRef.current = null
}
}, []);
}, [])
useEffect(() => () => clearPendingAlignment(), [clearPendingAlignment]);
useEffect(() => () => clearPendingAlignment(), [clearPendingAlignment])
const getScrollNode = useCallback(() => {
const node = scrollViewRef.current;
const node = scrollViewRef.current
if (!node) {
return null;
return null
}
if (typeof node.scrollTo === "function") {
return node;
if (typeof node.scrollTo === 'function') {
return node
}
if (typeof node.getNode === "function") {
return node.getNode();
if (typeof node.getNode === 'function') {
return node.getNode()
}
return null;
}, []);
return null
}, [])
const scrollToIndex = useCallback(
(index, animated = true, heightOverride) => {
const target = getScrollNode();
const target = getScrollNode()
if (!target) {
return;
return
}
const clamped = clampIndex(index);
const height =
heightOverride && heightOverride > 0 ? heightOverride : itemHeight;
const clamped = clampIndex(index)
const height = heightOverride && heightOverride > 0 ? heightOverride : itemHeight
if (!height) {
return;
return
}
updateSnapHeight(height);
const offset = clamped * height;
updateSnapHeight(height)
const offset = clamped * height
try {
if (typeof target.scrollTo === "function") {
target.scrollTo({ y: offset, animated });
} else if (typeof target.scrollToOffset === "function") {
target.scrollToOffset({ offset, animated });
if (typeof target.scrollTo === 'function') {
target.scrollTo({ y: offset, animated })
} else if (typeof target.scrollToOffset === 'function') {
target.scrollToOffset({ offset, animated })
}
} catch (_error) {
// ScrollView not ready yet, ignore.
}
activeIndexRef.current = clamped;
activeIndexRef.current = clamped
},
[clampIndex, getScrollNode, itemHeight, updateSnapHeight]
);
)
useEffect(() => {
if (
scrollViewRef.current == null ||
typeof activeIndex !== "number" ||
typeof activeIndex !== 'number' ||
activeIndex < 0 ||
activeIndex >= carouselItems.length ||
(isWeb && !itemHeight)
) {
return;
return
}
scrollToIndex(activeIndex);
}, [activeIndex, carouselItems.length, isWeb, itemHeight, scrollToIndex]);
scrollToIndex(activeIndex)
}, [activeIndex, carouselItems.length, isWeb, itemHeight, scrollToIndex])
useEffect(() => {
if (scrollViewRef.current == null || (isWeb && !itemHeight)) {
return;
return
}
scrollToIndex(activeIndexRef.current, false);
}, [carouselItems.length, isWeb, itemHeight, scrollToIndex]);
scrollToIndex(activeIndexRef.current, false)
}, [carouselItems.length, isWeb, itemHeight, scrollToIndex])
useEffect(() => {
if (!isWeb || !isFocused) {
return;
return
}
if (!itemHeight) {
return;
return
}
clearPendingAlignment();
scrollToIndex(activeIndexRef.current, false);
}, [
clearPendingAlignment,
isFocused,
isWeb,
itemHeight,
scrollToIndex,
]);
clearPendingAlignment()
scrollToIndex(activeIndexRef.current, false)
}, [clearPendingAlignment, isFocused, isWeb, itemHeight, scrollToIndex])
const alignToOffset = useCallback(
(offset, layoutHeight, options = {}) => {
const { forceSnap = false } = options || {};
const height =
layoutHeight && layoutHeight > 0 ? layoutHeight : itemHeight;
const { forceSnap = false } = options || {}
const height = layoutHeight && layoutHeight > 0 ? layoutHeight : itemHeight
if (!height) {
return;
return
}
updateSnapHeight(height);
updateSnapHeight(height)
const currentIndex = activeIndexRef.current;
const rawIndex = height ? offset / height : currentIndex;
const currentIndex = activeIndexRef.current
const rawIndex = height ? offset / height : currentIndex
let nextIndex = currentIndex;
let nextIndex = currentIndex
if (isWeb) {
const delta = rawIndex - currentIndex;
const delta = rawIndex - currentIndex
if (Math.abs(delta) > WEB_SCROLL_INACTIVE_DELTA) {
if (Math.abs(delta) <= 1) {
nextIndex = clampIndex(currentIndex + (delta > 0 ? 1 : -1));
nextIndex = clampIndex(currentIndex + (delta > 0 ? 1 : -1))
} else {
nextIndex = clampIndex(currentIndex + Math.round(delta));
nextIndex = clampIndex(currentIndex + Math.round(delta))
}
}
} else {
nextIndex = clampIndex(Math.round(rawIndex));
nextIndex = clampIndex(Math.round(rawIndex))
}
const hasChanged = nextIndex !== activeIndexRef.current;
const hasChanged = nextIndex !== activeIndexRef.current
if (hasChanged) {
activeIndexRef.current = nextIndex;
const callback = onActiveIndexChangeRef.current;
activeIndexRef.current = nextIndex
const callback = onActiveIndexChangeRef.current
if (callback) {
callback(nextIndex);
callback(nextIndex)
}
}
if (forceSnap || hasChanged) {
scrollToIndex(nextIndex, true, height);
scrollToIndex(nextIndex, true, height)
}
},
[clampIndex, isWeb, itemHeight, scrollToIndex, updateSnapHeight]
);
)
const handleScrollEnd = useCallback(
(event) => {
clearPendingAlignment();
const offsetY = event?.nativeEvent?.contentOffset?.y ?? 0;
const layoutHeight = event?.nativeEvent?.layoutMeasurement?.height;
alignToOffset(offsetY, layoutHeight, { forceSnap: true });
clearPendingAlignment()
const offsetY = event?.nativeEvent?.contentOffset?.y ?? 0
const layoutHeight = event?.nativeEvent?.layoutMeasurement?.height
alignToOffset(offsetY, layoutHeight, { forceSnap: true })
},
[alignToOffset, clearPendingAlignment]
);
)
const handleScroll = useCallback(
(event) => {
if (!isWeb) {
return;
return
}
const offsetY = event?.nativeEvent?.contentOffset?.y ?? 0;
const layoutHeight = event?.nativeEvent?.layoutMeasurement?.height;
clearPendingAlignment();
const offsetY = event?.nativeEvent?.contentOffset?.y ?? 0
const layoutHeight = event?.nativeEvent?.layoutMeasurement?.height
clearPendingAlignment()
alignTimeoutRef.current = globalThis.setTimeout(() => {
alignToOffset(offsetY, layoutHeight, { forceSnap: false });
alignTimeoutRef.current = null;
}, 80);
alignToOffset(offsetY, layoutHeight, { forceSnap: false })
alignTimeoutRef.current = null
}, 80)
},
[alignToOffset, clearPendingAlignment, isWeb]
);
)
const animatedScrollHandler = useMemo(
() =>
@@ -319,19 +301,19 @@ const FeatureCarousel = ({
listener: isWeb ? handleScroll : undefined,
}),
[handleScroll, isWeb, scrollY]
);
)
const handleLayout = useCallback(
(event) => {
const layoutHeight = event?.nativeEvent?.layout?.height ?? 0;
const layoutHeight = event?.nativeEvent?.layout?.height ?? 0
if (layoutHeight > 0) {
updateSnapHeight(layoutHeight);
updateSnapHeight(layoutHeight)
}
},
[updateSnapHeight]
);
)
const hasBackgroundImage = !!backgroundImage;
const hasBackgroundImage = !!backgroundImage
const renderContent = useMemo(
() =>
@@ -345,43 +327,35 @@ const FeatureCarousel = ({
]}
>
<View style={styles.cardWrapper}>
<PersonaCard
item={item}
index={index}
isLock={item.isLocked}
height={itemHeight}
/>
<PersonaCard item={item} index={index} isLock={item.isLocked} height={itemHeight} />
</View>
</View>
)),
[carouselItems, hasBackgroundImage, itemHeight]
);
)
const snapOffsets = useMemo(() => {
if (!itemHeight || !isWeb) {
return undefined;
return undefined
}
return carouselItems.map((_, index) => index * itemHeight);
}, [carouselItems, isWeb, itemHeight]);
return carouselItems.map((_, index) => index * itemHeight)
}, [carouselItems, isWeb, itemHeight])
const blurIntensity = isWeb ? 80 : 30;
const blurIntensity = isWeb ? 80 : 30
const dotsWrapperStyle = useMemo(
() => [
styles.dotsWrapperBase,
isWeb ? styles.dotsWrapperWeb : styles.dotsWrapperNative,
],
() => [styles.dotsWrapperBase, isWeb ? styles.dotsWrapperWeb : styles.dotsWrapperNative],
[isWeb]
);
)
const containerProps = hasBackgroundImage
? {
source: backgroundImage,
resizeMode: "cover",
resizeMode: 'cover',
imageStyle: styles.backgroundImage,
}
: {};
: {}
const ContainerComponent = hasBackgroundImage ? ImageBackground : View;
const ContainerComponent = hasBackgroundImage ? ImageBackground : View
return (
<ContainerComponent
@@ -396,11 +370,7 @@ const FeatureCarousel = ({
>
{hasBackgroundImage && (
<LinearGradient
colors={[
"rgba(66, 91, 135, 0)",
"rgba(66, 91, 135, 0.4)",
HOME_BACKGROUND_COLOR,
]}
colors={['rgba(66, 91, 135, 0)', 'rgba(66, 91, 135, 0.4)', HOME_BACKGROUND_COLOR]}
locations={[0, 0.75, 1]}
style={styles.backgroundGradient}
pointerEvents="none"
@@ -416,7 +386,7 @@ const FeatureCarousel = ({
snapToAlignment="start"
snapToInterval={!isWeb && itemHeight ? itemHeight : undefined}
snapToOffsets={snapOffsets}
decelerationRate={!isWeb ? "fast" : "normal"}
decelerationRate={!isWeb ? 'fast' : 'normal'}
style={styles.list}
contentContainerStyle={styles.scrollContent}
onScroll={animatedScrollHandler}
@@ -427,7 +397,7 @@ const FeatureCarousel = ({
</Animated.ScrollView>
<BlurView
intensity={blurIntensity}
tint={Platform.OS === "web" ? undefined : "dark"}
tint={Platform.OS === 'web' ? undefined : 'dark'}
style={dotsWrapperStyle}
pointerEvents="none"
>
@@ -444,16 +414,16 @@ const FeatureCarousel = ({
/>
</BlurView>
</ContainerComponent>
);
};
)
}
export default FeatureCarousel;
export default FeatureCarousel
const styles = StyleSheet.create({
container: {
flex: 1,
width: "100%",
flexDirection: "row",
width: '100%',
flexDirection: 'row',
},
containerWeb: {
// paddingRight: 56,
@@ -462,7 +432,7 @@ const styles = StyleSheet.create({
backgroundColor: HOME_BACKGROUND_COLOR,
},
containerTransparent: {
backgroundColor: "transparent",
backgroundColor: 'transparent',
},
list: {
flex: 1,
@@ -477,45 +447,45 @@ const styles = StyleSheet.create({
...StyleSheet.absoluteFillObject,
},
slide: {
width: "100%",
justifyContent: "flex-start",
width: '100%',
justifyContent: 'flex-start',
},
slideColored: {
backgroundColor: HOME_BACKGROUND_COLOR,
},
slideTransparent: {
backgroundColor: "transparent",
backgroundColor: 'transparent',
},
cardWrapper: {
flex: 1,
width: "60%",
justifyContent: "center",
alignSelf: "center",
width: '60%',
justifyContent: 'center',
alignSelf: 'center',
zIndex: 1,
},
dotsWrapperBase: {
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
borderRadius: 16,
paddingVertical: 12,
paddingHorizontal: 8,
overflow: "hidden",
backgroundColor: "rgba(18, 18, 18, 0.2)",
overflow: 'hidden',
backgroundColor: 'rgba(18, 18, 18, 0.2)',
},
dotsWrapperWeb: {
position: "absolute",
position: 'absolute',
right: 12,
top: 0,
bottom: 0,
maxHeight: "75%",
alignSelf: "center",
maxHeight: '75%',
alignSelf: 'center',
},
dotsWrapperNative: {
marginLeft: 12,
alignSelf: "center",
alignSelf: 'center',
},
dotsContainer: {
flexDirection: "column",
flexDirection: 'column',
},
dot: {
width: 8,
@@ -523,6 +493,6 @@ const styles = StyleSheet.create({
marginHorizontal: 0,
marginVertical: 6,
borderRadius: 999,
backgroundColor: "rgba(255,255,255,0.4)",
backgroundColor: 'rgba(255,255,255,0.4)',
},
});
})
+36 -46
View File
@@ -1,14 +1,14 @@
import { useContext, useGlobal } from "reactn";
import { ScrollView, Text, View, Image, Pressable } from "react-native";
import { useContext, useGlobal } from 'reactn'
import { ScrollView, Text, View, Image, Pressable } from 'react-native'
import firebase, { arrayRemove } from "../config/firebase";
import firebase, { arrayRemove } from '../config/firebase'
import { getFileNameFromURL } from "../helpers";
import { Fonts, Palette, Style, gutters } from "../styles";
import { icons } from "../assets";
import { getFileNameFromURL } from '../helpers'
import { Fonts, Palette, Style, gutters } from '../styles'
import { icons } from '../assets'
import { WebViewContext } from "../providers/WebViewProvider";
import alert from "./Alert";
import { WebViewContext } from '../providers/WebViewProvider'
import alert from './Alert'
export default ({
files = [],
@@ -17,67 +17,61 @@ export default ({
collectionRef = null,
containerStyle = {},
}) => {
const [, setIsLoading] = useGlobal("_isLoading");
const [, setTooltip] = useGlobal("_tooltip");
const [, setIsLoading] = useGlobal('_isLoading')
const [, setTooltip] = useGlobal('_tooltip')
const { setWebViewUrl } = useContext(WebViewContext);
const { setWebViewUrl } = useContext(WebViewContext)
const onDeleteFile = async (url) => {
alert(
"Êtes-vous sûr ?",
"Cette action est irréversible.",
'Êtes-vous sûr ?',
'Cette action est irréversible.',
[
{
text: "Annuler",
style: "cancel",
text: 'Annuler',
style: 'cancel',
},
{
text: "Confirmer",
text: 'Confirmer',
onPress: async () => {
try {
setIsLoading(true);
setIsLoading(true)
setFiles(
files.filter((file) => file !== url && file.uri !== url)
);
await firebase.storage().refFromURL(url).delete();
setFiles(files.filter((file) => file !== url && file.uri !== url))
await firebase.storage().refFromURL(url).delete()
if (documentID) {
await collectionRef.doc(documentID).update({
files: arrayRemove(url),
});
})
}
setTooltip({
type: "success",
text: "Fichier supprimé avec succès !",
});
type: 'success',
text: 'Fichier supprimé avec succès !',
})
} catch (error) {
console.log(error);
console.log(error)
} finally {
setIsLoading(false);
setIsLoading(false)
}
},
style: "confirm",
style: 'confirm',
},
],
{ cancelable: false }
);
};
)
}
if (files.length === 0) {
return null;
return null
}
return (
<View>
<ScrollView
horizontal
style={{ ...containerStyle }}
showsHorizontalScrollIndicator={false}
>
<ScrollView horizontal style={{ ...containerStyle }} showsHorizontalScrollIndicator={false}>
{files.map((file, index) => {
const uri = file.uri || file;
const uri = file.uri || file
return (
<View
@@ -97,7 +91,7 @@ export default ({
<Text
numberOfLines={1}
style={Fonts({
type: "default",
type: 'default',
color: Palette.white,
style: {
maxWidth: files?.length > 1 ? 100 : 200,
@@ -119,16 +113,12 @@ export default ({
)}
<Pressable onPress={() => setWebViewUrl(uri)}>
<Image
source={icons.eye}
style={Style.iconDefault}
resizeMode="contain"
/>
<Image source={icons.eye} style={Style.iconDefault} resizeMode="contain" />
</Pressable>
</View>
);
)
})}
</ScrollView>
</View>
);
};
)
}
+57 -73
View File
@@ -1,102 +1,86 @@
import React, { useCallback, useEffect, useRef } from "react";
import { View, Pressable, Text } from "react-native";
import { VideoView, useVideoPlayer } from "expo-video";
import { videos } from "../assets";
import { Portal } from "@gorhom/portal";
import React, { useCallback, useEffect, useRef } from 'react'
import { View, Pressable, Text } from 'react-native'
import { VideoView, useVideoPlayer } from 'expo-video'
import { videos } from '../assets'
import { Portal } from '@gorhom/portal'
// Fullscreen vertical video overlay without controls
// Props:
// - url?: string | number (require), source of the video. Defaults to videos.test
// - visible?: boolean, when false returns null
// - onClose: () => void, called when user skips or when video ends
const CLOSE_THRESHOLD_SECONDS = 0.35;
const CLOSE_THRESHOLD_SECONDS = 0.35
const FullscreenIntroVideo = ({ url, visible = true, onClose }) => {
const source = url
? typeof url === "string"
? { uri: url }
: url
: videos.test;
const source = url ? (typeof url === 'string' ? { uri: url } : url) : videos.test
const hasClosedRef = useRef(false);
const hasClosedRef = useRef(false)
const handleClose = useCallback(() => {
if (hasClosedRef.current) return;
hasClosedRef.current = true;
if (hasClosedRef.current) return
hasClosedRef.current = true
try {
onClose?.();
onClose?.()
} catch (e) {}
}, [onClose]);
}, [onClose])
const player = useVideoPlayer(source, (p) => {
p.loop = false;
p.timeUpdateEventInterval = 0.25;
});
p.loop = false
p.timeUpdateEventInterval = 0.25
})
useEffect(() => {
if (visible) {
hasClosedRef.current = false;
hasClosedRef.current = false
} else {
hasClosedRef.current = true;
hasClosedRef.current = true
try {
player?.pause?.();
player?.pause?.()
} catch (e) {}
}
}, [player, visible]);
}, [player, visible])
useEffect(() => {
if (!visible) {
return;
return
}
try {
player?.play?.();
player?.play?.()
} catch (e) {}
}, [player, visible]);
}, [player, visible])
useEffect(() => {
if (!player || !visible) return;
const playToEndSub = player.addListener?.("playToEnd", handleClose);
const timeUpdateSub = player.addListener?.(
"timeUpdate",
({ currentTime } = {}) => {
if (!player?.duration || hasClosedRef.current) {
return;
}
const remaining = player.duration - currentTime;
if (
Number.isFinite(remaining) &&
remaining <= CLOSE_THRESHOLD_SECONDS
) {
handleClose();
}
if (!player || !visible) return
const playToEndSub = player.addListener?.('playToEnd', handleClose)
const timeUpdateSub = player.addListener?.('timeUpdate', ({ currentTime } = {}) => {
if (!player?.duration || hasClosedRef.current) {
return
}
);
const playingChangeSub = player.addListener?.(
"playingChange",
({ isPlaying } = {}) => {
if (isPlaying || !player?.duration || hasClosedRef.current) {
return;
}
const remaining = player.duration - (player.currentTime ?? 0);
if (
Number.isFinite(remaining) &&
remaining <= CLOSE_THRESHOLD_SECONDS
) {
handleClose();
}
const remaining = player.duration - currentTime
if (Number.isFinite(remaining) && remaining <= CLOSE_THRESHOLD_SECONDS) {
handleClose()
}
);
})
const playingChangeSub = player.addListener?.('playingChange', ({ isPlaying } = {}) => {
if (isPlaying || !player?.duration || hasClosedRef.current) {
return
}
const remaining = player.duration - (player.currentTime ?? 0)
if (Number.isFinite(remaining) && remaining <= CLOSE_THRESHOLD_SECONDS) {
handleClose()
}
})
return () => {
try {
playToEndSub?.remove?.();
timeUpdateSub?.remove?.();
playingChangeSub?.remove?.();
playToEndSub?.remove?.()
timeUpdateSub?.remove?.()
playingChangeSub?.remove?.()
} catch (e) {}
};
}, [handleClose, player, visible]);
}
}, [handleClose, player, visible])
if (!visible) {
return null;
return null
}
return (
@@ -104,15 +88,15 @@ const FullscreenIntroVideo = ({ url, visible = true, onClose }) => {
<View
pointerEvents="box-none"
style={{
position: "absolute",
position: 'absolute',
paddingHorizontal: 10,
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "black",
justifyContent: "center",
alignItems: "center",
backgroundColor: 'black',
justifyContent: 'center',
alignItems: 'center',
zIndex: 9999,
}}
>
@@ -133,22 +117,22 @@ const FullscreenIntroVideo = ({ url, visible = true, onClose }) => {
<Pressable
onPress={handleClose}
style={{
position: "absolute",
position: 'absolute',
top: 50,
right: 20,
backgroundColor: "#00000080",
backgroundColor: '#00000080',
paddingVertical: 10,
paddingHorizontal: 14,
borderRadius: 20,
borderWidth: 1,
borderColor: "#FFFFFF55",
borderColor: '#FFFFFF55',
}}
>
<Text style={{ color: "#FFF", fontSize: 14 }}>Passer la vidéo</Text>
<Text style={{ color: '#FFF', fontSize: 14 }}>Passer la vidéo</Text>
</Pressable>
</View>
</Portal>
);
};
)
}
export default FullscreenIntroVideo;
export default FullscreenIntroVideo
+87 -87
View File
@@ -1,183 +1,183 @@
import { Portal } from "@gorhom/portal";
import { Asset } from "expo-asset";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Pressable, Text, View } from "react-native";
import { Portal } from '@gorhom/portal'
import { Asset } from 'expo-asset'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { Pressable, Text, View } from 'react-native'
const CLOSE_THRESHOLD_SECONDS = 0.35;
const CLOSE_POLL_INTERVAL_MS = 500;
const CLOSE_THRESHOLD_SECONDS = 0.35
const CLOSE_POLL_INTERVAL_MS = 500
const overlayStyle = {
position: "fixed",
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
left: 0,
backgroundColor: "black",
justifyContent: "center",
alignItems: "center",
backgroundColor: 'black',
justifyContent: 'center',
alignItems: 'center',
zIndex: 9999,
};
}
const videoStyle = {
position: "absolute",
position: 'absolute',
top: 0,
left: 0,
width: "100%",
height: "100%",
objectFit: "cover",
};
width: '100%',
height: '100%',
objectFit: 'cover',
}
const closeButtonStyle = {
position: "absolute",
position: 'absolute',
top: 50,
right: 20,
backgroundColor: "#00000080",
backgroundColor: '#00000080',
paddingVertical: 10,
paddingHorizontal: 14,
borderRadius: 20,
borderWidth: 1,
borderColor: "#FFFFFF55",
cursor: "pointer",
};
borderColor: '#FFFFFF55',
cursor: 'pointer',
}
const closeTextStyle = {
color: "#FFF",
color: '#FFF',
fontSize: 14,
};
}
const resolveModuleUri = async (module) => {
const asset = Asset.fromModule(module);
const asset = Asset.fromModule(module)
if (!asset.localUri && !asset.uri) {
await asset.downloadAsync();
await asset.downloadAsync()
}
return asset.localUri ?? asset.uri ?? null;
};
return asset.localUri ?? asset.uri ?? null
}
const FullscreenIntroVideo = ({ url, visible = true, onClose }) => {
const videoRef = useRef(null);
const [uri, setUri] = useState(null);
const [muted, setMuted] = useState(false);
const hasClosedRef = useRef(false);
const videoRef = useRef(null)
const [uri, setUri] = useState(null)
const [muted, setMuted] = useState(false)
const hasClosedRef = useRef(false)
const handleClose = useCallback(() => {
if (hasClosedRef.current) return;
hasClosedRef.current = true;
onClose?.();
}, [onClose]);
if (hasClosedRef.current) return
hasClosedRef.current = true
onClose?.()
}, [onClose])
const evaluateShouldClose = useCallback(() => {
const video = videoRef.current;
const video = videoRef.current
if (!video || hasClosedRef.current) {
return;
return
}
if (video.ended) {
handleClose();
return;
handleClose()
return
}
const remaining = video.duration - video.currentTime;
const remaining = video.duration - video.currentTime
if (Number.isFinite(remaining) && remaining <= CLOSE_THRESHOLD_SECONDS) {
handleClose();
handleClose()
}
}, [handleClose]);
}, [handleClose])
useEffect(() => {
let isMounted = true;
let isMounted = true
const assignUri = (nextUri) => {
if (isMounted) {
setMuted(false);
setUri(nextUri);
setMuted(false)
setUri(nextUri)
}
};
}
if (typeof url === "string") {
assignUri(url);
if (typeof url === 'string') {
assignUri(url)
return () => {
isMounted = false;
};
isMounted = false
}
}
const load = async () => {
try {
const nextUri = await resolveModuleUri(url);
assignUri(nextUri);
const nextUri = await resolveModuleUri(url)
assignUri(nextUri)
} catch {
if (isMounted) {
setUri(null);
setUri(null)
}
}
};
}
load();
load()
return () => {
isMounted = false;
};
}, [url]);
isMounted = false
}
}, [url])
useEffect(() => {
if (!visible || !uri) {
return undefined;
return undefined
}
hasClosedRef.current = false;
let rafId;
hasClosedRef.current = false
let rafId
const attemptPlay = () => {
const video = videoRef.current;
const video = videoRef.current
if (!video) {
rafId = requestAnimationFrame(attemptPlay);
return;
rafId = requestAnimationFrame(attemptPlay)
return
}
video.currentTime = 0;
const result = video.play();
video.currentTime = 0
const result = video.play()
if (result?.catch) {
result.catch((error) => {
if (error?.name === "NotAllowedError" && !muted) {
setMuted(true);
if (error?.name === 'NotAllowedError' && !muted) {
setMuted(true)
}
});
})
}
};
}
attemptPlay();
const pollId = setInterval(evaluateShouldClose, CLOSE_POLL_INTERVAL_MS);
attemptPlay()
const pollId = setInterval(evaluateShouldClose, CLOSE_POLL_INTERVAL_MS)
return () => {
if (rafId) {
cancelAnimationFrame(rafId);
cancelAnimationFrame(rafId)
}
clearInterval(pollId);
const video = videoRef.current;
video?.pause();
};
}, [evaluateShouldClose, muted, uri, visible]);
clearInterval(pollId)
const video = videoRef.current
video?.pause()
}
}, [evaluateShouldClose, muted, uri, visible])
useEffect(() => {
const video = videoRef.current;
const video = videoRef.current
if (!video || !muted) {
return;
return
}
const result = video.play();
const result = video.play()
if (result?.catch) {
result.catch(() => {});
result.catch(() => {})
}
}, [muted]);
}, [muted])
if (!visible) {
return null;
return null
}
return (
@@ -204,7 +204,7 @@ const FullscreenIntroVideo = ({ url, visible = true, onClose }) => {
</Pressable>
</View>
</Portal>
);
};
)
}
export default FullscreenIntroVideo;
export default FullscreenIntroVideo
+17 -18
View File
@@ -1,40 +1,39 @@
import React from "react";
import { Image, Pressable, Text } from "react-native";
import { Palette, Style } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { size } from "../styles/Style";
import { LinearGradient } from "./LinearGradient/LinearGradient";
import React from 'react'
import { Image, Pressable, Text } from 'react-native'
import { Palette, Style } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
import { size } from '../styles/Style'
import { LinearGradient } from './LinearGradient/LinearGradient'
const HEIGHT_BY_SIZE = {
small: 44,
medium: 50,
large: 58,
};
}
const FONT_SIZE_BY_SIZE = {
small: 14,
medium: 15,
large: 17,
};
}
const GradientButton = ({
title = "",
colors = ["#F94697", "#7023F7"],
title = '',
colors = ['#F94697', '#7023F7'],
onPress,
props,
containerStyle = {},
icon,
disabled = false,
maxWidth = null,
size = "medium",
size = 'medium',
textStyle = {},
gradientStyle = {},
height = null,
}) => {
const resolvedSize = HEIGHT_BY_SIZE[size] ? size : "medium";
const buttonHeight =
typeof height === "number" ? height : HEIGHT_BY_SIZE[resolvedSize];
const fontSize = FONT_SIZE_BY_SIZE[resolvedSize];
const resolvedSize = HEIGHT_BY_SIZE[size] ? size : 'medium'
const buttonHeight = typeof height === 'number' ? height : HEIGHT_BY_SIZE[resolvedSize]
const fontSize = FONT_SIZE_BY_SIZE[resolvedSize]
return (
<Pressable
@@ -79,7 +78,7 @@ const GradientButton = ({
</Text>
</LinearGradient>
</Pressable>
);
};
)
}
export default GradientButton;
export default GradientButton
+9 -9
View File
@@ -1,24 +1,24 @@
import Hyperlink from "react-native-hyperlink";
import Hyperlink from 'react-native-hyperlink'
import { useWebView } from "../providers/WebViewProvider";
import { Palette } from "../styles";
import { useWebView } from '../providers/WebViewProvider'
import { Palette } from '../styles'
const HyperlinkContainer = ({ children }) => {
const { setWebViewUrl } = useWebView();
const { setWebViewUrl } = useWebView()
return (
<Hyperlink
onPress={(url) => {
setWebViewUrl(url);
setWebViewUrl(url)
}}
linkStyle={{
color: Palette.primary,
textDecorationLine: "underline",
textDecorationLine: 'underline',
}}
>
{children}
</Hyperlink>
);
};
)
}
export default HyperlinkContainer;
export default HyperlinkContainer
+8 -8
View File
@@ -1,7 +1,7 @@
import { Image, View } from "react-native";
import { Image, View } from 'react-native'
import { Palette, Style } from "../styles";
import { gutters, mainBorderRadius } from "../styles/Style";
import { Palette, Style } from '../styles'
import { gutters, mainBorderRadius } from '../styles/Style'
const IconContainer = ({ icon }) => {
return (
@@ -19,13 +19,13 @@ const IconContainer = ({ icon }) => {
source={icon}
resizeMode="contain"
style={{
width: "50%",
height: "50%",
width: '50%',
height: '50%',
tintColor: Palette.primary,
}}
/>
</View>
);
};
)
}
export default IconContainer;
export default IconContainer
+25 -27
View File
@@ -1,29 +1,28 @@
import React, { useState, useEffect } from "reactn";
import { Image, Pressable, Text, View } from "react-native";
import { FlatGrid } from "react-native-super-grid";
import React, { useState, useEffect } from 'reactn'
import { Image, Pressable, Text, View } from 'react-native'
import { FlatGrid } from 'react-native-super-grid'
import { responsiveWidth } from "../actions/responsiveSizes.js";
import { responsiveWidth } from '../actions/responsiveSizes.js'
import { Fonts, Palette, Style, gutters } from "../styles";
import { mainBorderRadius } from "../styles/Style";
import { Fonts, Palette, Style, gutters } from '../styles'
import { mainBorderRadius } from '../styles/Style'
import useLayoutType from "../hooks/useLayoutType.js";
import useLayoutType from '../hooks/useLayoutType.js'
const IconSelector = ({ onClose } = {}) => {
const { isNative } = useLayoutType();
const { isNative } = useLayoutType()
const [currentIconIndex, setCurrentIconIndex] = useState(0);
const [currentIconIndex, setCurrentIconIndex] = useState(0)
useEffect(() => {
if (isNative) {
// import("expo-dynamic-app-icon").then((module) => {
// getAppIcon = module.getAppIcon;
// const iconIndex = getAppIcon();
// setCurrentIconIndex(Number(iconIndex));
// });
}
}, []);
}, [])
return (
<FlatGrid
@@ -31,8 +30,8 @@ const IconSelector = ({ onClose } = {}) => {
<View>
<Text
style={{
...Fonts({ type: "title" }),
textAlign: "center",
...Fonts({ type: 'title' }),
textAlign: 'center',
marginBottom: gutters / 2,
}}
>
@@ -40,10 +39,10 @@ const IconSelector = ({ onClose } = {}) => {
</Text>
<Text
style={{
...Fonts({ type: "default" }),
textAlign: "center",
width: "60%",
alignSelf: "center",
...Fonts({ type: 'default' }),
textAlign: 'center',
width: '60%',
alignSelf: 'center',
marginBottom: gutters,
}}
>
@@ -61,13 +60,13 @@ const IconSelector = ({ onClose } = {}) => {
style={{ flex: 1, backgroundColor: Palette.darkPurple }}
spacing={10}
renderItem={({ item, index }) => {
const isSelected = currentIconIndex === index + 1;
const isSelected = currentIconIndex === index + 1
return (
<Pressable
key={index}
style={{
width: "100%",
width: '100%',
height: responsiveWidth(45),
...Style.containerCenter,
...Style.defaultShadows,
@@ -75,7 +74,6 @@ const IconSelector = ({ onClose } = {}) => {
onPress={() => {
// import("expo-dynamic-app-icon").then((module) => {
// setAppIcon = module.setAppIcon;
// setAppIcon((index + 1).toString());
// setCurrentIconIndex(index + 1);
// onClose?.();
@@ -86,10 +84,10 @@ const IconSelector = ({ onClose } = {}) => {
source={item}
resizeMode="cover"
style={{
width: "90%",
height: "90%",
width: '90%',
height: '90%',
borderRadius: mainBorderRadius * 2,
overflow: "hidden",
overflow: 'hidden',
...(isSelected && {
borderColor: Palette.primary,
borderWidth: 2,
@@ -97,10 +95,10 @@ const IconSelector = ({ onClose } = {}) => {
}}
/>
</Pressable>
);
)
}}
/>
);
};
)
}
export default IconSelector;
export default IconSelector
+64 -76
View File
@@ -1,30 +1,22 @@
import { useState } from "react";
import {
Image,
InputAccessoryView,
Keyboard,
Pressable,
Text,
TextInput,
View,
} from "react-native";
import { useState } from 'react'
import { Image, InputAccessoryView, Keyboard, Pressable, Text, TextInput, View } from 'react-native'
// import CountryPicker, { DARK_THEME } from "react-native-country-picker-modal";
import usePlaceApi from "react-native-minuit/src/hooks/usePlacesApi";
import usePlaceApi from 'react-native-minuit/src/hooks/usePlacesApi'
import { icons } from "../assets";
import { Fonts, Palette, Style, gutters } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { icons } from '../assets'
import { Fonts, Palette, Style, gutters } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
import { BlurView } from "expo-blur";
import EyeSlashSVG from "../assets/UI/EyeSlashSVG";
import EyeSVG from "../assets/UI/EyeSVG";
import { GOOGLE_API_KEY } from "../data/keys";
import { isWeb } from "../hooks/useLayoutType";
import { BlurView } from 'expo-blur'
import EyeSlashSVG from '../assets/UI/EyeSlashSVG'
import EyeSVG from '../assets/UI/EyeSVG'
import { GOOGLE_API_KEY } from '../data/keys'
import { isWeb } from '../hooks/useLayoutType'
const Input = ({
inputRef = null,
label = "",
placeholder = "",
label = '',
placeholder = '',
containerStyle = {},
textInputStyle = {},
@@ -34,48 +26,46 @@ const Input = ({
textInputProps = {},
type = "default", // "default" | "password" | "textarea" | "coinAmount" | "countryPicker" | "autoCompleteAddress"
theme = "default", // "default" | "radioactiv" | "dashed"
type = 'default', // "default" | "password" | "textarea" | "coinAmount" | "countryPicker" | "autoCompleteAddress"
theme = 'default', // "default" | "radioactiv" | "dashed"
borderType = "none", // "solid" | "dashed" | "none"
borderType = 'none', // "solid" | "dashed" | "none"
layout = "default", // "default" | "line"
layout = 'default', // "default" | "line"
isNumeric = false,
isBlur = false,
}) => {
const [isFocused, setIsFocused] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [isFocused, setIsFocused] = useState(false)
const [showPassword, setShowPassword] = useState(false)
const isDefaultLayout = layout === "default";
const isDefaultLayout = layout === 'default'
const mainColor =
theme === "radioactiv" ? Palette.radioactivGreen : Palette.primary;
const mainColor = theme === 'radioactiv' ? Palette.radioactivGreen : Palette.primary
const isRoundedRectangle =
["textarea", "coinAmount"].includes(type) || layout === "default";
const isRoundedRectangle = ['textarea', 'coinAmount'].includes(type) || layout === 'default'
const inputAccessoryViewID = "uniqueID";
const inputAccessoryViewID = 'uniqueID'
const { places } = usePlaceApi({
query: type === "autoCompleteAddress" ? value : "",
query: type === 'autoCompleteAddress' ? value : '',
apiKey: GOOGLE_API_KEY, // Your Google API Key
queryFields: "formatted_address,geometry,name,address_components",
queryCountries: ["fr"],
language: "fr-FR",
queryFields: 'formatted_address,geometry,name,address_components',
queryCountries: ['fr'],
language: 'fr-FR',
minChars: 2,
});
})
const ContainerView = isBlur ? BlurView : View;
const ContainerView = isBlur ? BlurView : View
const resolvedKeyboardType =
textInputProps?.keyboardType ??
(isNumeric ? "numeric" : type === "email" ? "email-address" : "default");
(isNumeric ? 'numeric' : type === 'email' ? 'email-address' : 'default')
return (
<>
<View
style={{
width: "100%",
width: '100%',
...containerStyle,
gap: 4,
}}
@@ -106,11 +96,11 @@ const Input = ({
? {
paddingVertical: 10,
backgroundColor:
type === "coinAmount"
type === 'coinAmount'
? Palette.transparentRadioactivGreen
: Palette.glass,
height: type === "textarea" ? 150 : 50,
overflow: "hidden",
height: type === 'textarea' ? 150 : 50,
overflow: 'hidden',
borderRadius: 12,
}
: {}),
@@ -120,68 +110,66 @@ const Input = ({
borderBottomColor: mainColor,
borderBottomWidth: 1,
}),
...(borderType === "dashed"
...(borderType === 'dashed'
? {
borderStyle: "dashed",
borderColor: isFocused
? Palette.primary
: Palette.transparentPrimary,
borderStyle: 'dashed',
borderColor: isFocused ? Palette.primary : Palette.transparentPrimary,
borderWidth: 1,
}
: {}),
width: "100%",
width: '100%',
}}
>
{type === "search" ? (
{type === 'search' ? (
<Image
source={icons.search}
style={[Style.iconDefault, { marginRight: 10 }]}
resizeMode="contain"
/>
) : null}
{type !== "countryPicker" && (
{type !== 'countryPicker' && (
<TextInput
ref={inputRef}
placeholder={placeholder}
placeholderTextColor={Palette.gray}
value={value}
onChangeText={setValue}
multiline={type === "textarea"}
editable={type !== "countryPicker"}
multiline={type === 'textarea'}
editable={type !== 'countryPicker'}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
style={{
width: "100%",
width: '100%',
...(isRoundedRectangle
? {
height: "100%",
height: '100%',
flex: 1,
textAlignVertical: type === "textarea" ? "top" : "center",
textAlignVertical: type === 'textarea' ? 'top' : 'center',
}
: { textAlign: "center" }),
: { textAlign: 'center' }),
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
...(isWeb
? {
lineHeight: "auto",
lineHeight: 'auto',
}
: {}),
...textInputStyle,
}}
{...(type === "password"
{...(type === 'password'
? {
secureTextEntry: !showPassword,
autoCapitalize: "none",
autoCompleteType: "password",
textContentType: "password",
autoCapitalize: 'none',
autoCompleteType: 'password',
textContentType: 'password',
}
: {})}
{...(type === "email"
{...(type === 'email'
? {
autoCapitalize: "none",
autoCompleteType: "email",
textContentType: "emailAddress",
autoCapitalize: 'none',
autoCompleteType: 'email',
textContentType: 'emailAddress',
}
: {})}
keyboardType={resolvedKeyboardType}
@@ -190,21 +178,21 @@ const Input = ({
{...textInputProps}
/>
)}
{type === "password" ? (
{type === 'password' ? (
<Pressable onPress={() => setShowPassword(!showPassword)}>
{showPassword ? <EyeSVG /> : <EyeSlashSVG />}
</Pressable>
) : null}
</ContainerView>
{type === "autoCompleteAddress" &&
{type === 'autoCompleteAddress' &&
places?.[0]?.description &&
places?.[0]?.description !== value &&
places.map((place, index) => (
<Pressable
key={index}
onPress={() => {
setValue(place?.description);
setValue(place?.description)
}}
style={{
...Style.containerSpaceBetween,
@@ -216,19 +204,19 @@ const Input = ({
...Fonts({}),
}}
>
{place?.description || "-"}
{place?.description || '-'}
</Text>
</Pressable>
))}
</View>
{(type === "textarea" || isNumeric) && !isWeb && (
{(type === 'textarea' || isNumeric) && !isWeb && (
<InputAccessoryView nativeID={inputAccessoryViewID}>
<Pressable
onPress={() => Keyboard.dismiss()}
style={{
...Style.containerRow,
justifyContent: "flex-end",
justifyContent: 'flex-end',
backgroundColor: Palette.transparentPrimary,
padding: gutters,
paddingVertical: gutters / 2,
@@ -245,7 +233,7 @@ const Input = ({
</InputAccessoryView>
)}
</>
);
};
)
}
export { Input };
export { Input }
+51 -70
View File
@@ -1,78 +1,69 @@
import { useState } from "react";
import { View, Text, Image, Pressable, StyleSheet } from "react-native";
import { BlurView } from "expo-blur";
import { useState } from 'react'
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 Switch from '../components/Switch'
import OptionSelector from '../components/OptionSelector'
import { Container, Title, Input, Button } from '../components/Dialog'
import { Fonts, Style, gutters } from "../styles";
import { icons } from "../assets";
import { Fonts, Style, gutters } from '../styles'
import { icons } from '../assets'
const labelOptions = {
name: "Nouveau nom",
email: "Nouvelle adresse email",
password: "Nouveau mot de passe",
language: "Nouvelle langue",
};
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",
};
fr: 'Français',
en: 'English',
es: 'Español',
de: 'Deutsch',
it: 'Italiano',
}
export default ({ itemKey, type, value, title, onUpdateValue }) => {
const [showDialog, setShowDialog] = useState(false);
const [inputData, setInputData] = useState(value);
const [currentPassword, setCurrentPassword] = useState("");
const [showDialog, setShowDialog] = useState(false)
const [inputData, setInputData] = useState(value)
const [currentPassword, setCurrentPassword] = useState('')
return (
<>
<View style={{}}>
<View style={Style.containerSpaceBetween}>
<Text style={Fonts({ type: "section" })}>{title}</Text>
{type === "boolean" ? (
<Text style={Fonts({ type: 'section' })}>{title}</Text>
{type === 'boolean' ? (
<Switch
value={value}
setValue={(newValue) =>
onUpdateValue({ key: itemKey, value: newValue })
}
setValue={(newValue) => onUpdateValue({ key: itemKey, value: newValue })}
/>
) : (
<Pressable
onPress={() => setShowDialog(true)}
style={[
Style.containerRow,
{ maxWidth: "50%", justifyContent: "flex-end" },
]}
style={[Style.containerRow, { maxWidth: '50%', justifyContent: 'flex-end' }]}
>
<Text
numberOfLines={1}
style={Fonts({
type: "section",
type: 'section',
style: {
opacity: 0.5,
textAlign: "right",
width: "80%",
textAlign: 'right',
width: '80%',
marginRight: gutters,
},
})}
>
{itemKey === "password"
? "********"
: itemKey === "language"
? languageOptions[value] || value
: value}
{itemKey === 'password'
? '********'
: itemKey === 'language'
? languageOptions[value] || value
: value}
</Text>
<Image
source={icons.edit}
style={Style.iconDefault}
resizeMode="contain"
/>
<Image source={icons.edit} style={Style.iconDefault} resizeMode="contain" />
</Pressable>
)}
</View>
@@ -83,37 +74,33 @@ export default ({ itemKey, type, value, title, onUpdateValue }) => {
<Container
visible={showDialog}
blurComponentIOS={
<BlurView
style={StyleSheet.absoluteFill}
blurType="xdark"
blurAmount={50}
/>
<BlurView style={StyleSheet.absoluteFill} blurType="xdark" blurAmount={50} />
}
>
<Title>{`Changement ${title?.toLowerCase()}`}</Title>
{["password", "email"].includes(itemKey) && (
{['password', 'email'].includes(itemKey) && (
<Input
label="Mot de passe actuel"
value={currentPassword}
onChangeText={(text) => setCurrentPassword(text)}
keyboardType="visible-password"
type={"password"}
type={'password'}
containerStyle={{ marginBottom: gutters / 2 }}
/>
)}
{itemKey === "language" ? (
{itemKey === 'language' ? (
<OptionSelector
optionTypeList={languageOptions}
selected={inputData || "fr"}
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" },
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' },
}}
/>
) : (
@@ -121,9 +108,7 @@ export default ({ itemKey, type, value, title, onUpdateValue }) => {
label={labelOptions[itemKey]}
value={inputData}
onChangeText={(text) => setInputData(text)}
autoCapitalize={
["password", "email"].includes(itemKey) ? "none" : "words"
}
autoCapitalize={['password', 'email'].includes(itemKey) ? 'none' : 'words'}
type={itemKey}
containerStyle={{ marginBottom: gutters / 2 }}
/>
@@ -131,16 +116,12 @@ export default ({ itemKey, type, value, title, onUpdateValue }) => {
<Button
label="Valider"
onPress={() => {
onUpdateValue({ key: itemKey, value: inputData, currentPassword });
setShowDialog(false);
onUpdateValue({ key: itemKey, value: inputData, currentPassword })
setShowDialog(false)
}}
/>
<Button
label="Annuler"
onPress={() => setShowDialog(false)}
type={"secondary"}
/>
<Button label="Annuler" onPress={() => setShowDialog(false)} type={'secondary'} />
</Container>
</>
);
};
)
}
+16 -20
View File
@@ -1,9 +1,9 @@
import { useKeyboard } from "@react-native-community/hooks";
import { BlurView } from "expo-blur";
import React from "react";
import { Platform, StyleSheet, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import BorderGradient from "../BorderGradient/BorderGradient";
import { useKeyboard } from '@react-native-community/hooks'
import { BlurView } from 'expo-blur'
import React from 'react'
import { Platform, StyleSheet, View } from 'react-native'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import BorderGradient from '../BorderGradient/BorderGradient'
const ItemContainer = ({
height = responsiveHeight(40),
@@ -12,23 +12,19 @@ const ItemContainer = ({
style,
disableKeyboardHeight = false,
}) => {
const { keyboardShown } = useKeyboard();
const { keyboardShown } = useKeyboard()
return (
<BorderGradient
gradientProps={{
colors: ["#FFFFFF00", "#FFFFFF"],
colors: ['#FFFFFF00', '#FFFFFF'],
start: { x: 0.3, y: 0 },
end: { x: 1, y: 1 },
...gradientProps,
}}
style={{
...styles.borderGradientStyle,
height: !disableKeyboardHeight
? keyboardShown
? responsiveHeight(30)
: height
: height,
height: !disableKeyboardHeight ? (keyboardShown ? responsiveHeight(30) : height) : height,
...style,
}}
@@ -40,23 +36,23 @@ const ItemContainer = ({
android: 100,
web: 100,
})}
tint={"dark"}
tint={'dark'}
style={{ flex: 1, padding: 6 }}
>
{children}
</BlurView>
</View>
</BorderGradient>
);
};
)
}
export default ItemContainer;
export default ItemContainer
const styles = StyleSheet.create({
borderGradientStyle: {
borderWidth: 1,
borderRadius: 20,
shadowColor: "#000",
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
@@ -68,7 +64,7 @@ const styles = StyleSheet.create({
blurContainer: {
flex: 1,
borderRadius: 20,
overflow: "hidden",
overflow: 'hidden',
zIndex: 1,
},
});
})
@@ -1,27 +1,27 @@
import { View, StyleSheet } from "react-native";
import React, { useMemo, useState } from "react";
import omit from "lodash/omit";
import BorderGradient from "../BorderGradient/BorderGradient";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { BlurView } from "expo-blur";
import { Style } from "../../styles";
import { View, StyleSheet } from 'react-native'
import React, { useMemo, useState } from 'react'
import omit from 'lodash/omit'
import BorderGradient from '../BorderGradient/BorderGradient'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { BlurView } from 'expo-blur'
import { Style } from '../../styles'
const ITEM_BORDER_RADIUS = 24;
const ITEM_BORDER_RADIUS = 24
const CONTAINER_STYLE_PROPS = [
"margin",
"marginTop",
"marginBottom",
"marginLeft",
"marginRight",
"marginHorizontal",
"marginVertical",
"alignSelf",
"alignItems",
"justifyContent",
"width",
"minWidth",
"maxWidth",
];
'margin',
'marginTop',
'marginBottom',
'marginLeft',
'marginRight',
'marginHorizontal',
'marginVertical',
'alignSelf',
'alignItems',
'justifyContent',
'width',
'minWidth',
'maxWidth',
]
const ItemContainer = ({
height = responsiveHeight(40),
@@ -32,33 +32,33 @@ const ItemContainer = ({
style,
disableKeyboardHeight = false, // parity with native signature
}) => {
const [containerLayout, setContainerLayout] = useState(null);
const flattenedStyle = StyleSheet.flatten(style) || {};
const [containerLayout, setContainerLayout] = useState(null)
const flattenedStyle = StyleSheet.flatten(style) || {}
const containerStyleOverrides = useMemo(() => {
return CONTAINER_STYLE_PROPS.reduce((acc, key) => {
if (typeof flattenedStyle[key] !== "undefined") {
acc[key] = flattenedStyle[key];
if (typeof flattenedStyle[key] !== 'undefined') {
acc[key] = flattenedStyle[key]
}
return acc;
}, {});
}, [flattenedStyle]);
return acc
}, {})
}, [flattenedStyle])
const {
width: styleWidth,
maxWidth: styleMaxWidth,
minWidth: styleMinWidth,
...remainingContainerStyle
} = containerStyleOverrides;
} = containerStyleOverrides
const gradientStyleOverrides = useMemo(
() => omit(flattenedStyle, CONTAINER_STYLE_PROPS),
[flattenedStyle]
);
)
const baseHeight = typeof height === "number" ? height : undefined;
const measuredHeight = containerLayout?.height ?? baseHeight;
const resolvedWidth = styleWidth ?? width ?? "100%";
const resolvedMaxWidth = styleMaxWidth ?? maxWidth;
const baseHeight = typeof height === 'number' ? height : undefined
const measuredHeight = containerLayout?.height ?? baseHeight
const resolvedWidth = styleWidth ?? width ?? '100%'
const resolvedMaxWidth = styleMaxWidth ?? maxWidth
return (
<View
@@ -72,7 +72,7 @@ const ItemContainer = ({
>
<BorderGradient
gradientProps={{
colors: ["rgba(72, 51, 51, 0)", "#FFFFFF"],
colors: ['rgba(72, 51, 51, 0)', '#FFFFFF'],
start: { x: 0.3, y: 0 },
end: { x: 1, y: 1 },
locations: [0, 1],
@@ -91,32 +91,28 @@ const ItemContainer = ({
baseHeight ? { height: baseHeight } : null,
]}
onLayout={(e) => {
setContainerLayout(e.nativeEvent.layout);
setContainerLayout(e.nativeEvent.layout)
}}
>
<BlurView
intensity={disableKeyboardHeight ? 35 : 45}
tint="dark"
style={styles.blurView}
>
<BlurView intensity={disableKeyboardHeight ? 35 : 45} tint="dark" style={styles.blurView}>
{children}
</BlurView>
</View>
</View>
);
};
)
}
export default ItemContainer;
export default ItemContainer
const styles = StyleSheet.create({
wrapper: {
position: "relative",
alignSelf: "stretch",
position: 'relative',
alignSelf: 'stretch',
},
borderGradientStyle: {
borderWidth: 1.2,
borderRadius: ITEM_BORDER_RADIUS,
shadowColor: "rgba(3, 0, 18, 0.58)",
shadowColor: 'rgba(3, 0, 18, 0.58)',
shadowOffset: {
width: 0,
height: 2,
@@ -124,21 +120,21 @@ const styles = StyleSheet.create({
shadowOpacity: 0.22,
shadowRadius: 18,
elevation: 8,
position: "absolute",
width: "100%",
position: 'absolute',
width: '100%',
},
contentWrapper: {
borderRadius: ITEM_BORDER_RADIUS,
overflow: "hidden",
overflow: 'hidden',
zIndex: 1,
},
blurView: {
flex: 1,
width: "100%",
height: "100%",
width: '100%',
height: '100%',
paddingHorizontal: 32,
paddingVertical: 28,
justifyContent: "center",
alignSelf: "stretch",
justifyContent: 'center',
alignSelf: 'stretch',
},
});
})
+13 -19
View File
@@ -1,11 +1,11 @@
import React from "react";
import { Image, Pressable, Text, View } from "react-native";
import React from 'react'
import { Image, Pressable, Text, View } from 'react-native'
import { responsiveHeight } from "../actions/responsiveSizes";
import { responsiveHeight } from '../actions/responsiveSizes'
import { Fonts, Palette, Style } from "../styles";
import { icons } from "../assets";
import IconContainer from "./IconContainer";
import { Fonts, Palette, Style } from '../styles'
import { icons } from '../assets'
import IconContainer from './IconContainer'
const ItemRowList = ({
title,
@@ -14,7 +14,7 @@ const ItemRowList = ({
textStyle = {},
addMarginTopFromPrevious = false,
containerStyle = {},
separatorPosition = "bottom",
separatorPosition = 'bottom',
}) => {
return (
<View
@@ -23,27 +23,21 @@ const ItemRowList = ({
...containerStyle,
}}
>
{separatorPosition === "top" && (
<View style={Style.separatorHorizontal} />
)}
{separatorPosition === 'top' && <View style={Style.separatorHorizontal} />}
<Pressable onPress={action} style={Style.containerSpaceBetween}>
<View style={Style.containerRow}>
{icon && <IconContainer icon={icon} />}
<Text style={Fonts({ type: "section", style: textStyle })}>
{title}
</Text>
<Text style={Fonts({ type: 'section', style: textStyle })}>{title}</Text>
</View>
<Image source={icons.arrowRight} style={Style.iconSmall} />
</Pressable>
{separatorPosition === "bottom" && (
<View style={Style.separatorHorizontal} />
)}
{separatorPosition === 'bottom' && <View style={Style.separatorHorizontal} />}
</View>
);
};
)
}
export default ItemRowList;
export default ItemRowList
+50 -60
View File
@@ -1,72 +1,68 @@
import React, { useMemo } from "react";
import { Text, View } from "react-native";
import { Palette } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import React, { useMemo } from 'react'
import { Text, View } from 'react-native'
import { Palette } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
export function groupAlignedWordsToLines(alignedWords = [], { removeTags = true } = {}) {
const out = [];
let buf = [];
let start = null;
const out = []
let buf = []
let start = null
const clean = (txt) =>
String(txt || "")
.replace(/\s+/g, " ")
.trim();
const isSentenceEnd = (txt) => /[.!?]$/.test((txt || "").trim());
const isSectionTag = (txt) => /^\s*\[[^\]]+\]\s*$/i.test((txt || "").trim());
String(txt || '')
.replace(/\s+/g, ' ')
.trim()
const isSentenceEnd = (txt) => /[.!?]$/.test((txt || '').trim())
const isSectionTag = (txt) => /^\s*\[[^\]]+\]\s*$/i.test((txt || '').trim())
for (let i = 0; i < alignedWords.length; i++) {
const w = alignedWords[i] || {};
const original = String(w.word || "");
const textNoNewline = original.replace(/\n/g, " ");
const textNoTag = removeTags
? textNoNewline.replace(/^\s*\[[^\]]+\]\s*/i, "")
: textNoNewline;
const next = alignedWords[i + 1] || null;
const gapToNext = next
? Math.max(0, Number(next.startS || 0) - Number(w.endS || 0))
: 0;
const w = alignedWords[i] || {}
const original = String(w.word || '')
const textNoNewline = original.replace(/\n/g, ' ')
const textNoTag = removeTags ? textNoNewline.replace(/^\s*\[[^\]]+\]\s*/i, '') : textNoNewline
const next = alignedWords[i + 1] || null
const gapToNext = next ? Math.max(0, Number(next.startS || 0) - Number(w.endS || 0)) : 0
if (buf.length === 0) start = Number(w.startS || 0);
if (buf.length === 0) start = Number(w.startS || 0)
if (isSectionTag(textNoNewline)) {
if (!removeTags) {
const joined = clean(textNoNewline);
const joined = clean(textNoNewline)
if (joined)
out.push({
text: joined,
startS: start ?? Number(w.startS || 0),
endS: Number(w.endS || 0),
});
})
}
buf = [];
start = null;
continue;
buf = []
start = null
continue
}
if (!clean(textNoTag)) {
continue;
continue
}
buf.push(textNoTag);
buf.push(textNoTag)
const eolByNewline = /\n/.test(original);
const eolByPause = gapToNext >= 0.6; // threshold for a logical break
const eolByPunct = isSentenceEnd(textNoTag);
const isLast = i === alignedWords.length - 1;
const eolByNewline = /\n/.test(original)
const eolByPause = gapToNext >= 0.6 // threshold for a logical break
const eolByPunct = isSentenceEnd(textNoTag)
const isLast = i === alignedWords.length - 1
if (eolByNewline || eolByPause || eolByPunct || isLast) {
const joined = clean(buf.join(" "));
const joined = clean(buf.join(' '))
if (joined)
out.push({
text: joined,
startS: start ?? Number(w.startS || 0),
endS: Number(w.endS || 0),
});
buf = [];
start = null;
})
buf = []
start = null
}
}
return out;
return out
}
export default function KaraokeLyrics({
@@ -78,30 +74,24 @@ export default function KaraokeLyrics({
const lines = useMemo(
() => groupAlignedWordsToLines(alignedWords, { removeTags }),
[alignedWords, removeTags]
);
)
const currentLineIdx = useMemo(() => {
if (!lines || lines.length === 0) return -1;
if (!lines || lines.length === 0) return -1
for (let i = 0; i < lines.length; i++) {
const L = lines[i];
if (currentTimeS >= (L.startS || 0) && currentTimeS <= (L.endS || 0))
return i;
const L = lines[i]
if (currentTimeS >= (L.startS || 0) && currentTimeS <= (L.endS || 0)) return i
}
if (currentTimeS > (lines[lines.length - 1]?.endS || 0))
return lines.length - 1;
return -1;
}, [lines, currentTimeS]);
if (currentTimeS > (lines[lines.length - 1]?.endS || 0)) return lines.length - 1
return -1
}, [lines, currentTimeS])
if (!lines.length) return null;
if (!lines.length) return null
const prev =
showContext && currentLineIdx > 0 ? lines[currentLineIdx - 1]?.text : "";
const curr =
currentLineIdx >= 0 ? lines[currentLineIdx]?.text : lines[0]?.text;
const prev = showContext && currentLineIdx > 0 ? lines[currentLineIdx - 1]?.text : ''
const curr = currentLineIdx >= 0 ? lines[currentLineIdx]?.text : lines[0]?.text
const next =
showContext && currentLineIdx + 1 < lines.length
? lines[currentLineIdx + 1]?.text
: "";
showContext && currentLineIdx + 1 < lines.length ? lines[currentLineIdx + 1]?.text : ''
return (
<View style={{ gap: 4 }}>
@@ -121,7 +111,7 @@ export default function KaraokeLyrics({
style={{
color: Palette.white,
fontSize: 18,
textAlign: "center",
textAlign: 'center',
fontFamily: FONT_FAMILY.InterSemiBold,
}}
>
@@ -130,9 +120,9 @@ export default function KaraokeLyrics({
{next ? (
<Text
style={{
color: "#FFFFFF99",
color: '#FFFFFF99',
fontSize: 14,
textAlign: "center",
textAlign: 'center',
fontFamily: FONT_FAMILY.InterMedium,
}}
>
@@ -140,5 +130,5 @@ export default function KaraokeLyrics({
</Text>
) : null}
</View>
);
)
}
+107 -137
View File
@@ -1,20 +1,19 @@
import { BlurView } from "expo-blur";
import React, { useEffect, useMemo, useState } from "react";
import { FlatList, Platform, SectionList, Text, View } from "react-native";
import useLayoutType from "../../hooks/useLayoutType";
import CreateLyricsHeader from "../../screens/Writing/components/CreateLyricsHeader";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { LinearGradient } from "../LinearGradient/LinearGradient";
import { BlurView } from 'expo-blur'
import React, { useEffect, useMemo, useState } from 'react'
import { FlatList, Platform, SectionList, Text, View } from 'react-native'
import useLayoutType from '../../hooks/useLayoutType'
import CreateLyricsHeader from '../../screens/Writing/components/CreateLyricsHeader'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { LinearGradient } from '../LinearGradient/LinearGradient'
const BLUE_SELECTION_GRADIENT = ["#4673F9", "#7023F7"];
const BLUE_SELECTION_GRADIENT = ['#4673F9', '#7023F7']
const buildItemKey = (value, category) =>
category ? `${category}::${value}` : `${value}`;
const buildItemKey = (value, category) => (category ? `${category}::${value}` : `${value}`)
const ListSelection = ({
options = [],
variant = "simple",
variant = 'simple',
selected: selectedProp,
setSelected: setSelectedProp,
multiple = false,
@@ -27,92 +26,88 @@ const ListSelection = ({
itemContainerStyle,
itemOuterStyle,
itemTextStyle,
highlightColor = "#F94697",
highlightColor = '#F94697',
disableHover = false,
}) => {
// état interne si non contrôlé
const [internalSelected, setInternalSelected] = useState(
variant === "sectioned" ? {} : multiple ? [] : null
);
const selected = selectedProp !== undefined ? selectedProp : internalSelected;
const setSelected =
setSelectedProp !== undefined ? setSelectedProp : setInternalSelected;
variant === 'sectioned' ? {} : multiple ? [] : null
)
const selected = selectedProp !== undefined ? selectedProp : internalSelected
const setSelected = setSelectedProp !== undefined ? setSelectedProp : setInternalSelected
const { isWeb } = useLayoutType();
const [hoveredKey, setHoveredKey] = useState(null);
const { isWeb } = useLayoutType()
const [hoveredKey, setHoveredKey] = useState(null)
useEffect(() => {
if (disableHover && hoveredKey !== null) setHoveredKey(null);
}, [disableHover, hoveredKey]);
if (disableHover && hoveredKey !== null) setHoveredKey(null)
}, [disableHover, hoveredKey])
// helpers
const valueOf = useMemo(() => {
if (typeof getItemValue === "function") return getItemValue;
if (variant === "simple") return (item) => item;
return (item) => item?.title ?? item;
}, [getItemValue, variant]);
if (typeof getItemValue === 'function') return getItemValue
if (variant === 'simple') return (item) => item
return (item) => item?.title ?? item
}, [getItemValue, variant])
const formatValue = (item) =>
typeof formatSelectedValue === "function"
? formatSelectedValue(item)
: valueOf(item);
typeof formatSelectedValue === 'function' ? formatSelectedValue(item) : valueOf(item)
const extractSelectedComparable = (s) => {
if (typeof selectedValueExtractor === "function")
return selectedValueExtractor(s);
if (s && typeof s === "object" && "title" in s) return s.title;
return s;
};
if (typeof selectedValueExtractor === 'function') return selectedValueExtractor(s)
if (s && typeof s === 'object' && 'title' in s) return s.title
return s
}
const isSelected = (val, category) => {
if (variant === "sectioned") return selected?.[category] === val;
if (variant === 'sectioned') return selected?.[category] === val
if (multiple) {
const list = Array.isArray(selected) ? selected : [];
return list.some((v) => v === val);
const list = Array.isArray(selected) ? selected : []
return list.some((v) => v === val)
}
return extractSelectedComparable(selected) === val;
};
return extractSelectedComparable(selected) === val
}
const toggleSelect = (item, category) => {
const val = valueOf(item);
const val = valueOf(item)
if (variant === "sectioned") {
const current = selected && typeof selected === "object" ? selected : {};
const next = { ...current };
if (current?.[category] === val) next[category] = null;
else next[category] = formatValue(item);
setSelected(next);
return;
if (variant === 'sectioned') {
const current = selected && typeof selected === 'object' ? selected : {}
const next = { ...current }
if (current?.[category] === val) next[category] = null
else next[category] = formatValue(item)
setSelected(next)
return
}
if (multiple) {
const list = Array.isArray(selected) ? selected : [];
const exists = list.some((v) => v === val);
if (exists) setSelected(list.filter((v) => v !== val));
const list = Array.isArray(selected) ? selected : []
const exists = list.some((v) => v === val)
if (exists) setSelected(list.filter((v) => v !== val))
else if (!maxSelection || list.length < maxSelection)
setSelected([...list, formatValue(item)]);
setSelected([...list, formatValue(item)])
} else {
if (extractSelectedComparable(selected) === val) setSelected(null);
else setSelected(formatValue(item));
if (extractSelectedComparable(selected) === val) setSelected(null)
else setSelected(formatValue(item))
}
};
}
// contenus élémentaires : toujours encapsuler le texte dans <Text>
const renderSimpleContent = (label) => (
<View style={{ minHeight: 40, justifyContent: "center" }}>
<View style={{ minHeight: 40, justifyContent: 'center' }}>
<Text
style={{
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "left",
textAlign: 'left',
...itemTextStyle,
}}
>
{typeof label === "string" ? label : String(label)}
{typeof label === 'string' ? label : String(label)}
</Text>
</View>
);
)
const renderTitleDescriptionContent = (item) => (
<View style={{ paddingVertical: 6 }}>
@@ -125,14 +120,14 @@ const ListSelection = ({
}}
>
<Text style={{ fontFamily: FONT_FAMILY.InterBold }}>{item?.title}</Text>
<Text>{` : ${item?.description ?? ""}`}</Text>
<Text>{` : ${item?.description ?? ''}`}</Text>
</Text>
</View>
);
)
// applique le gradient bleu UNIQUEMENT sur web et quand sélectionné
const withWebBlueGradientIfSelected = (content, sel) => {
if (!isWeb || !sel) return content;
if (!isWeb || !sel) return content
// wrap dans un View pour éviter texte brut direct sous le gradient (compat RN Web)
return (
<LinearGradient
@@ -144,62 +139,53 @@ const ListSelection = ({
borderRadius: 14,
paddingHorizontal: 12,
paddingVertical: 8,
overflow: "hidden",
overflow: 'hidden',
}}
>
<View>{content}</View>
</LinearGradient>
);
};
)
}
// ============ SECTIONED ============
if (variant === "sectioned") {
if (variant === 'sectioned') {
return (
<SectionList
sections={options}
showsVerticalScrollIndicator={false}
contentContainerStyle={contentContainerStyle}
renderItem={({ item, section }) => {
const cat = section?.title;
const val = valueOf(item);
const sel = isSelected(val, cat);
const itemKey = buildItemKey(String(val ?? ""), cat);
const isHovered = isWeb && !disableHover && hoveredKey === itemKey;
const showHoverOutline = isHovered && !sel;
const showSelectionOutline = !isWeb && sel;
const cat = section?.title
const val = valueOf(item)
const sel = isSelected(val, cat)
const itemKey = buildItemKey(String(val ?? ''), cat)
const isHovered = isWeb && !disableHover && hoveredKey === itemKey
const showHoverOutline = isHovered && !sel
const showSelectionOutline = !isWeb && sel
const baseContent = renderSimpleContent(item);
const baseContent = renderSimpleContent(item)
const headerContainerStyle = {
borderWidth: 0,
borderColor: "transparent",
borderColor: 'transparent',
borderRadius: 14,
...itemContainerStyle,
};
if (sel && isWeb)
headerContainerStyle.backgroundColor = "transparent";
}
if (sel && isWeb) headerContainerStyle.backgroundColor = 'transparent'
return (
<View
style={[{ position: "relative" }, itemOuterStyle]}
onMouseEnter={
isWeb && !disableHover
? () => setHoveredKey(itemKey)
: undefined
}
onMouseLeave={
isWeb && !disableHover ? () => setHoveredKey(null) : undefined
}
style={[{ position: 'relative' }, itemOuterStyle]}
onMouseEnter={isWeb && !disableHover ? () => setHoveredKey(itemKey) : undefined}
onMouseLeave={isWeb && !disableHover ? () => setHoveredKey(null) : undefined}
>
<CreateLyricsHeader
onPress={() => toggleSelect(item, cat)}
tint={sel ? "default" : "dark"}
tint={sel ? 'default' : 'dark'}
colors={[Palette.tran, Palette.tran]}
showBorder={!sel}
disableBlur={sel && isWeb}
blurViewStyle={
sel && isWeb
? { paddingHorizontal: 0, paddingVertical: 0 }
: undefined
sel && isWeb ? { paddingHorizontal: 0, paddingVertical: 0 } : undefined
}
containerStyle={headerContainerStyle}
>
@@ -210,7 +196,7 @@ const ListSelection = ({
<View
pointerEvents="none"
style={{
position: "absolute",
position: 'absolute',
top: 0,
left: 0,
right: 0,
@@ -223,21 +209,17 @@ const ListSelection = ({
/>
)}
</View>
);
)
}}
renderSectionHeader={({ section: { title } }) => (
<View
style={{ alignSelf: "flex-start", marginLeft: 10, marginBottom: 6 }}
>
<View style={{ alignSelf: 'flex-start', marginLeft: 10, marginBottom: 6 }}>
<BlurView
intensity={40}
tint="dark"
experimentalBlurMethod={
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
}
experimentalBlurMethod={Platform.OS !== 'ios' ? 'dimezisBlurView' : 'none'}
style={{
borderRadius: 18,
overflow: "hidden",
overflow: 'hidden',
paddingHorizontal: 8,
paddingVertical: 4,
}}
@@ -256,7 +238,7 @@ const ListSelection = ({
)}
keyExtractor={(item, idx) => `${valueOf(item)}-${idx}`}
/>
);
)
}
// ============ FLAT (simple/titleDescription/emotion) ============
@@ -266,66 +248,54 @@ const ListSelection = ({
showsVerticalScrollIndicator={false}
contentContainerStyle={contentContainerStyle}
renderItem={({ item }) => {
const val = valueOf(item);
const sel = isSelected(val);
const useEmotionColors = variant === "emotion";
const val = valueOf(item)
const sel = isSelected(val)
const useEmotionColors = variant === 'emotion'
// pour "emotion", on masque le bord gauche si sélectionné
const borderColors = useEmotionColors
? sel
? [Palette.tran, Palette.tran]
: item?.color
: [Palette.tran, Palette.tran];
const tint = useEmotionColors ? "dark" : sel ? "default" : "dark";
: [Palette.tran, Palette.tran]
const tint = useEmotionColors ? 'dark' : sel ? 'default' : 'dark'
const itemKey = buildItemKey(String(val ?? ""));
const isHovered = isWeb && !disableHover && hoveredKey === itemKey;
const showHoverOutline = isHovered && !sel;
const itemKey = buildItemKey(String(val ?? ''))
const isHovered = isWeb && !disableHover && hoveredKey === itemKey
const showHoverOutline = isHovered && !sel
const showSelectionOutline =
!isWeb &&
(variant === "simple" ||
variant === "emotion" ||
variant === "titleDescription") &&
sel;
(variant === 'simple' || variant === 'emotion' || variant === 'titleDescription') &&
sel
const baseContent =
variant === "simple"
? renderSimpleContent(item)
: renderTitleDescriptionContent(item);
variant === 'simple' ? renderSimpleContent(item) : renderTitleDescriptionContent(item)
const headerContainerStyle = {
borderWidth: 0,
borderColor: "transparent",
borderColor: 'transparent',
borderRadius: 14,
...itemContainerStyle,
};
if (sel && isWeb) headerContainerStyle.backgroundColor = "transparent";
}
if (sel && isWeb) headerContainerStyle.backgroundColor = 'transparent'
const shouldShowBorder = !sel;
const shouldShowBorder = !sel
return (
<View
style={[{ position: "relative" }, itemOuterStyle]}
onMouseEnter={
isWeb && !disableHover ? () => setHoveredKey(itemKey) : undefined
}
onMouseLeave={
isWeb && !disableHover ? () => setHoveredKey(null) : undefined
}
style={[{ position: 'relative' }, itemOuterStyle]}
onMouseEnter={isWeb && !disableHover ? () => setHoveredKey(itemKey) : undefined}
onMouseLeave={isWeb && !disableHover ? () => setHoveredKey(null) : undefined}
>
<CreateLyricsHeader
colors={borderColors}
tint={tint}
onPress={() => toggleSelect(item)}
gradientProps={
useEmotionColors && !sel ? { locations: [0.24, 1] } : undefined
}
gradientProps={useEmotionColors && !sel ? { locations: [0.24, 1] } : undefined}
showBorder={shouldShowBorder}
disableBlur={sel && isWeb} // web: pas de blur si gradient
blurViewStyle={
sel && isWeb
? { paddingHorizontal: 0, paddingVertical: 0 }
: undefined
sel && isWeb ? { paddingHorizontal: 0, paddingVertical: 0 } : undefined
}
containerStyle={headerContainerStyle}
>
@@ -336,7 +306,7 @@ const ListSelection = ({
<View
pointerEvents="none"
style={{
position: "absolute",
position: 'absolute',
top: 0,
left: 0,
right: 0,
@@ -349,11 +319,11 @@ const ListSelection = ({
/>
)}
</View>
);
)
}}
keyExtractor={(item, idx) => `${valueOf(item)}-${idx}`}
/>
);
};
)
}
export default ListSelection;
export default ListSelection
+3 -3
View File
@@ -1,4 +1,4 @@
import NativeMaskedView from "@react-native-masked-view/masked-view";
import NativeMaskedView from '@react-native-masked-view/masked-view'
const MaskedView = NativeMaskedView;
export default MaskedView;
const MaskedView = NativeMaskedView
export default MaskedView
+4 -4
View File
@@ -1,8 +1,8 @@
import React from "react";
import { View } from "react-native";
import React from 'react'
import { View } from 'react-native'
function MaskedView({ maskElement, ...props }) {
return React.createElement(View, props, maskElement);
return React.createElement(View, props, maskElement)
}
export default MaskedView;
export default MaskedView
+39 -48
View File
@@ -1,58 +1,49 @@
import React, { useCallback, useMemo, useState } from "react";
import { Platform, Pressable, StyleSheet } from "react-native";
import CreditAmount from "./CreditAmount";
import CoinPackModal from "./modal/CoinPackModal";
import { useUser } from "../providers/UserDataProvider";
import { Palette } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { openCoinPackModal } from "../utils/coinPackModal";
import React, { useCallback, useMemo, useState } from 'react'
import { Platform, Pressable, StyleSheet } from 'react-native'
import CreditAmount from './CreditAmount'
import CoinPackModal from './modal/CoinPackModal'
import { useUser } from '../providers/UserDataProvider'
import { Palette } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
import { openCoinPackModal } from '../utils/coinPackModal'
const MobileCoinBadge = ({
style,
textStyle,
iconSize = 20,
iconPosition = "left",
}) => {
const { currentUID, currentUserData } = useUser() || {};
const [isModalVisible, setModalVisible] = useState(false);
const MobileCoinBadge = ({ style, textStyle, iconSize = 20, iconPosition = 'left' }) => {
const { currentUID, currentUserData } = useUser() || {}
const [isModalVisible, setModalVisible] = useState(false)
const coinBalance = useMemo(() => {
const value = currentUserData?.coins;
if (typeof value === "number" && Number.isFinite(value)) {
return value;
const value = currentUserData?.coins
if (typeof value === 'number' && Number.isFinite(value)) {
return value
}
if (typeof value === "string") {
const parsed = Number(value);
if (typeof value === 'string') {
const parsed = Number(value)
if (Number.isFinite(parsed)) {
return parsed;
return parsed
}
}
return 0;
}, [currentUserData?.coins]);
return 0
}, [currentUserData?.coins])
const handlePress = useCallback(() => {
if (Platform.OS === "web") {
openCoinPackModal();
return;
if (Platform.OS === 'web') {
openCoinPackModal()
return
}
setModalVisible(true);
}, []);
setModalVisible(true)
}, [])
const handleClose = useCallback(() => {
setModalVisible(false);
}, []);
setModalVisible(false)
}, [])
if (!currentUID || Platform.OS === "web") {
return null;
if (!currentUID || Platform.OS === 'web') {
return null
}
return (
<>
<Pressable
onPress={handlePress}
accessibilityRole="button"
style={[styles.container, style]}
>
<Pressable onPress={handlePress} accessibilityRole="button" style={[styles.container, style]}>
<CreditAmount
value={coinBalance}
style={styles.content}
@@ -63,25 +54,25 @@ const MobileCoinBadge = ({
</Pressable>
<CoinPackModal visible={isModalVisible} onClose={handleClose} />
</>
);
};
)
}
const styles = StyleSheet.create({
container: {
flexDirection: "row",
alignItems: "center",
flexDirection: 'row',
alignItems: 'center',
gap: 8,
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 20,
backgroundColor: "rgba(12, 14, 18, 0.72)",
backgroundColor: 'rgba(12, 14, 18, 0.72)',
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.1)",
borderColor: 'rgba(255, 255, 255, 0.1)',
flexShrink: 1,
},
content: {
flexDirection: "row",
alignItems: "center",
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
text: {
@@ -89,6 +80,6 @@ const styles = StyleSheet.create({
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
},
});
})
export default MobileCoinBadge;
export default MobileCoinBadge
+43 -48
View File
@@ -1,63 +1,62 @@
import React, { useContext, useEffect, useMemo, useState } from "react";
import { Platform, StyleSheet, View } from "react-native";
import { useGlobal } from "reactn";
import { Portal } from "@gorhom/portal";
import FullscreenIntroVideo from "./FullscreenIntroVideo";
import { useUser } from "../providers/UserDataProvider";
import { SplashAnimationContext } from "../providers/SplashAnimationProvider";
import { musiclandShareUrl } from "../data";
import React, { useContext, useEffect, useMemo, useState } from 'react'
import { Platform, StyleSheet, View } from 'react-native'
import { useGlobal } from 'reactn'
import { Portal } from '@gorhom/portal'
import FullscreenIntroVideo from './FullscreenIntroVideo'
import { useUser } from '../providers/UserDataProvider'
import { SplashAnimationContext } from '../providers/SplashAnimationProvider'
import { musiclandShareUrl } from '../data'
const BLOCKING_TIMEOUT_MS = 5000;
const BLOCKING_TIMEOUT_MS = 5000
// Affiche la vidéo d'intro mobile (videos.splash) juste après le splash screen.
const MobileSplashVideo = () => {
const [, setShareQrModal] = useGlobal("shareQrModal");
const { videos } = useUser();
const { isFullyLoaded } = useContext(SplashAnimationContext);
const [, setShareQrModal] = useGlobal('shareQrModal')
const { videos } = useUser()
const { isFullyLoaded } = useContext(SplashAnimationContext)
const [visible, setVisible] = useState(false);
const [hasShown, setHasShown] = useState(false);
const [gaveUp, setGaveUp] = useState(false);
const [visible, setVisible] = useState(false)
const [hasShown, setHasShown] = useState(false)
const [gaveUp, setGaveUp] = useState(false)
const splashVideoUrl = useMemo(() => {
if (Platform.OS === "web") return null;
return videos?.splash ?? null;
}, [videos]);
if (Platform.OS === 'web') return null
return videos?.splash ?? null
}, [videos])
const shouldAttempt =
Platform.OS !== "web" && isFullyLoaded && !hasShown && !gaveUp;
const showBlockingOverlay = shouldAttempt && !visible;
const shouldAttempt = Platform.OS !== 'web' && isFullyLoaded && !hasShown && !gaveUp
const showBlockingOverlay = shouldAttempt && !visible
useEffect(() => {
if (!shouldAttempt) return;
if (!splashVideoUrl) return;
setVisible(true);
}, [shouldAttempt, splashVideoUrl]);
if (!shouldAttempt) return
if (!splashVideoUrl) return
setVisible(true)
}, [shouldAttempt, splashVideoUrl])
useEffect(() => {
if (!showBlockingOverlay) return;
if (splashVideoUrl) return;
if (!showBlockingOverlay) return
if (splashVideoUrl) return
const timeoutId = setTimeout(() => {
setHasShown(true);
setGaveUp(true);
}, BLOCKING_TIMEOUT_MS);
setHasShown(true)
setGaveUp(true)
}, BLOCKING_TIMEOUT_MS)
return () => clearTimeout(timeoutId);
}, [showBlockingOverlay, splashVideoUrl]);
return () => clearTimeout(timeoutId)
}, [showBlockingOverlay, splashVideoUrl])
const handleClose = () => {
setVisible(false);
setHasShown(true);
setVisible(false)
setHasShown(true)
setShareQrModal({
visible: true,
url: musiclandShareUrl,
title: "Scannez le QR-code",
});
};
title: 'Scannez le QR-code',
})
}
if (!showBlockingOverlay && !visible) {
return null;
return null
}
return (
@@ -68,22 +67,18 @@ const MobileSplashVideo = () => {
</Portal>
) : null}
{visible ? (
<FullscreenIntroVideo
url={splashVideoUrl}
visible={visible}
onClose={handleClose}
/>
<FullscreenIntroVideo url={splashVideoUrl} visible={visible} onClose={handleClose} />
) : null}
</>
);
};
)
}
const styles = StyleSheet.create({
blocker: {
...StyleSheet.absoluteFillObject,
backgroundColor: "black",
backgroundColor: 'black',
zIndex: 9998,
},
});
})
export default MobileSplashVideo;
export default MobileSplashVideo
+43 -49
View File
@@ -1,31 +1,31 @@
import { BlurView } from "expo-blur";
import React from "react";
import { Platform, Pressable, Text, View } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import useMinuit from "react-native-minuit/src/hooks/useMinuit";
import Animated, { Easing, FadeIn, FadeOut } from "react-native-reanimated";
import { arrayRemove, playlistsRef } from "../config/firebase";
import { Palette } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { Portal } from "@gorhom/portal";
import { BlurView } from 'expo-blur'
import React from 'react'
import { Platform, Pressable, Text, View } from 'react-native'
import { SheetManager } from 'react-native-actions-sheet'
import useMinuit from 'react-native-minuit/src/hooks/useMinuit'
import Animated, { Easing, FadeIn, FadeOut } from 'react-native-reanimated'
import { arrayRemove, playlistsRef } from '../config/firebase'
import { Palette } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
import { Portal } from '@gorhom/portal'
const MoreBtn = ({ onPress, children }) => (
<Pressable onPress={onPress}>
<BlurView
intensity={Platform.OS !== "ios" ? 70 : 30}
intensity={Platform.OS !== 'ios' ? 70 : 30}
tint="dark"
style={{
paddingHorizontal: 12,
paddingVertical: 10,
backgroundColor: Palette.glass,
borderRadius: 12,
overflow: "hidden",
overflow: 'hidden',
}}
>
{children}
</BlurView>
</Pressable>
);
)
const MoreMenu = ({
visible = false,
@@ -37,68 +37,62 @@ const MoreMenu = ({
projectId = null,
extraItems = [],
}) => {
const { setTooltip } = useMinuit();
if (!visible) return null;
const { setTooltip } = useMinuit()
if (!visible) return null
const resolvedTop =
typeof position?.top === "number"
? position.top
: typeof top === "number"
? top
: 0;
const resolvedRight =
typeof position?.right === "number" ? Math.max(position.right, 0) : null;
const resolvedLeft =
typeof position?.left === "number" ? Math.max(position.left, 0) : null;
typeof position?.top === 'number' ? position.top : typeof top === 'number' ? top : 0
const resolvedRight = typeof position?.right === 'number' ? Math.max(position.right, 0) : null
const resolvedLeft = typeof position?.left === 'number' ? Math.max(position.left, 0) : null
const removeFromPlaylist = async () => {
try {
if (inPlaylist && playlistId && projectId) {
await playlistsRef
.doc(playlistId)
.update({ musics: arrayRemove(projectId), updatedAt: new Date() });
setTooltip({ type: "success", text: "Supprimé de la playlist" });
.update({ musics: arrayRemove(projectId), updatedAt: new Date() })
setTooltip({ type: 'success', text: 'Supprimé de la playlist' })
}
} catch (e) {
console.log("Remove from playlist error", e?.message);
console.log('Remove from playlist error', e?.message)
setTooltip({
type: "error",
text: e?.message || "Suppression impossible",
});
type: 'error',
text: e?.message || 'Suppression impossible',
})
} finally {
onClose?.();
onClose?.()
}
};
}
const addToPlaylist = () => {
if (projectId) {
SheetManager.show("Playlist", { payload: { projectId } });
SheetManager.show('Playlist', { payload: { projectId } })
} else {
SheetManager.show("Playlist");
SheetManager.show('Playlist')
}
onClose?.();
};
onClose?.()
}
const anchorStyle = {
position: "absolute",
position: 'absolute',
top: resolvedTop,
zIndex: 2,
elevation: 2,
};
}
if (resolvedRight != null) {
anchorStyle.right = resolvedRight;
anchorStyle.right = resolvedRight
} else if (resolvedLeft != null) {
anchorStyle.left = resolvedLeft;
anchorStyle.left = resolvedLeft
} else {
anchorStyle.right = 0;
anchorStyle.right = 0
}
return (
<Portal>
<View
style={{
position: "absolute",
position: 'absolute',
left: 0,
right: 0,
top: 0,
@@ -111,7 +105,7 @@ const MoreMenu = ({
<Pressable
onPress={onClose}
style={{
position: "absolute",
position: 'absolute',
left: 0,
right: 0,
top: 0,
@@ -157,9 +151,9 @@ const MoreMenu = ({
key={idx}
onPress={() => {
try {
item?.onPress?.();
item?.onPress?.()
} finally {
onClose?.();
onClose?.()
}
}}
>
@@ -170,7 +164,7 @@ const MoreMenu = ({
fontFamily: FONT_FAMILY.InterRegular,
}}
>
{item?.label || "Action"}
{item?.label || 'Action'}
</Text>
</MoreBtn>
))}
@@ -178,7 +172,7 @@ const MoreMenu = ({
</Animated.View>
</View>
</Portal>
);
};
)
}
export default MoreMenu;
export default MoreMenu
+22 -32
View File
@@ -1,9 +1,9 @@
import React from "react";
import { Image, Platform, Pressable, Text, View } from "react-native";
import { icons } from "../assets";
import { Palette, Style } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import ProgressBar from "./ProgressBar";
import React from 'react'
import { Image, Platform, Pressable, Text, View } from 'react-native'
import { icons } from '../assets'
import { Palette, Style } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
import ProgressBar from './ProgressBar'
const MusicLandHeader = ({
onPressBack,
@@ -13,10 +13,8 @@ const MusicLandHeader = ({
logo = null,
style,
}) => {
const isWeb = Platform.OS === "web";
const backHitSlop = isWeb
? undefined
: { top: 16, right: 16, bottom: 16, left: 16 };
const isWeb = Platform.OS === 'web'
const backHitSlop = isWeb ? undefined : { top: 16, right: 16, bottom: 16, left: 16 }
const backButtonStyle = isWeb
? {
...Style.containerRow,
@@ -27,29 +25,21 @@ const MusicLandHeader = ({
borderWidth: 1,
backgroundColor: Palette.ultraLightWhite,
borderColor: Palette.ultraLightWhite,
borderStyle: "solid",
borderStyle: 'solid',
}
: { width: 24, height: 24, ...Style.containerCenter };
const backIconSource = isWeb ? icons.chevronDown : icons.chevronDown;
: { width: 24, height: 24, ...Style.containerCenter }
const backIconSource = isWeb ? icons.chevronDown : icons.chevronDown
const backIconStyle = {
width: 15,
height: 15,
transform: [{ rotate: "90deg" }],
};
transform: [{ rotate: '90deg' }],
}
return (
<View style={{ alignItems: "center", gap: 16, ...style }}>
<View style={{ width: "100%", ...Style.containerRow, gap: 16 }}>
<Pressable
style={backButtonStyle}
onPress={onPressBack}
hitSlop={backHitSlop}
>
<Image
source={backIconSource}
style={backIconStyle}
resizeMode="contain"
/>
<View style={{ alignItems: 'center', gap: 16, ...style }}>
<View style={{ width: '100%', ...Style.containerRow, gap: 16 }}>
<Pressable style={backButtonStyle} onPress={onPressBack} hitSlop={backHitSlop}>
<Image source={backIconSource} style={backIconStyle} resizeMode="contain" />
{isWeb && (
<Text
style={{
@@ -84,14 +74,14 @@ const MusicLandHeader = ({
<Image
source={logo}
style={{
alignSelf: "center",
alignSelf: 'center',
height: isWeb ? 150 : 100,
resizeMode: "contain",
resizeMode: 'contain',
}}
/>
)}
</View>
);
};
)
}
export default MusicLandHeader;
export default MusicLandHeader
+14 -16
View File
@@ -1,13 +1,13 @@
import { Image, Pressable, View } from "react-native";
import { Motion } from "@legendapp/motion";
import { Image, Pressable, View } from 'react-native'
import { Motion } from '@legendapp/motion'
import { icons } from "../assets";
import { Fonts, gutters, Palette, Style } from "../styles";
import { goBack } from "../navigation/NavigationService";
import { FONT_FAMILY } from "../styles/Fonts";
import { icons } from '../assets'
import { Fonts, gutters, Palette, Style } from '../styles'
import { goBack } from '../navigation/NavigationService'
import { FONT_FAMILY } from '../styles/Fonts'
export default ({
title = "",
title = '',
rightComponent = null,
onBackPressed = null,
containerStyle = {},
@@ -19,7 +19,7 @@ export default ({
style={[
Style.containerSpaceBetween,
{
alignItems: "flex-start",
alignItems: 'flex-start',
marginBottom: gutters / 2,
paddingTop: 10,
...containerStyle,
@@ -37,7 +37,7 @@ export default ({
style={[
Style.iconSmall,
Style.mirrorHorizontal,
{ transform: [{ rotate: "90deg" }] },
{ transform: [{ rotate: '90deg' }] },
]}
resizeMode="contain"
/>
@@ -51,10 +51,10 @@ export default ({
exit={{ opacity: 0.2 }}
transition={{
default: {
type: "spring",
type: 'spring',
},
opacity: {
type: "timing",
type: 'timing',
},
}}
style={{
@@ -67,9 +67,7 @@ export default ({
{title}
</Motion.Text>
<View style={{ flex: 1, alignItems: "flex-end" }}>
{rightComponent?.() || null}
</View>
<View style={{ flex: 1, alignItems: 'flex-end' }}>{rightComponent?.() || null}</View>
</View>
);
};
)
}
+12 -14
View File
@@ -1,7 +1,7 @@
import { Pressable, Text, View } from "react-native";
import { Pressable, Text, View } from 'react-native'
import { Fonts } from "../styles";
import Style, { defaultItemHeight } from "../styles/Style";
import { Fonts } from '../styles'
import Style, { defaultItemHeight } from '../styles/Style'
const OptionSelector = ({
optionTypeList = {},
@@ -17,11 +17,11 @@ const OptionSelector = ({
...Style.containerItem,
padding: 0,
...containerStyle,
width: "100%",
width: '100%',
}}
>
{Object.entries(optionTypeList).map(([key, value], index) => {
const isSelected = key === selected;
const isSelected = key === selected
return (
<Pressable
@@ -34,25 +34,23 @@ const OptionSelector = ({
padding: 0,
height: defaultItemHeight,
margin: defaultItemHeight * 0.2,
backgroundColor: isSelected
? colorMap[key]?.secondary
: "transparent",
backgroundColor: isSelected ? colorMap[key]?.secondary : 'transparent',
borderWidth: 1,
borderColor: isSelected ? colorMap[key]?.primary : "transparent",
borderColor: isSelected ? colorMap[key]?.primary : 'transparent',
}}
>
<Text
style={{
...Fonts({ type: "section" }),
...Fonts({ type: 'section' }),
}}
>
{value}
</Text>
</Pressable>
);
)
})}
</View>
);
};
)
}
export default OptionSelector;
export default OptionSelector
+23 -26
View File
@@ -1,13 +1,13 @@
import React from "react";
import { Pressable, StyleSheet, View } from "react-native";
import { AnimatePresence, Motion } from "@legendapp/motion";
import { BlurView } from "expo-blur";
import { Portal } from "@gorhom/portal";
import React from 'react'
import { Pressable, StyleSheet, View } from 'react-native'
import { AnimatePresence, Motion } from '@legendapp/motion'
import { BlurView } from 'expo-blur'
import { Portal } from '@gorhom/portal'
import useLayoutType from "../hooks/useLayoutType";
import useLayoutType from '../hooks/useLayoutType'
import { Palette, Style } from "../styles";
import { defaultBlurIntensity } from "../styles/Style";
import { Palette, Style } from '../styles'
import { defaultBlurIntensity } from '../styles/Style'
const Overlay = ({
isVisible,
@@ -20,16 +20,13 @@ const Overlay = ({
hasPortal = true,
}) => {
const { isNative } = useLayoutType();
const { isNative } = useLayoutType()
const ContentContainerView = hasPortal ? Portal : React.Fragment;
const resolvedBlurIntensity =
blurIntensity ?? (isNative ? defaultBlurIntensity : 15);
const shouldUseBlur = resolvedBlurIntensity > 0;
const BackgroundView = shouldUseBlur ? BlurView : View;
const backgroundProps = shouldUseBlur
? { intensity: resolvedBlurIntensity, tint: "dark" }
: {};
const ContentContainerView = hasPortal ? Portal : React.Fragment
const resolvedBlurIntensity = blurIntensity ?? (isNative ? defaultBlurIntensity : 15)
const shouldUseBlur = resolvedBlurIntensity > 0
const BackgroundView = shouldUseBlur ? BlurView : View
const backgroundProps = shouldUseBlur ? { intensity: resolvedBlurIntensity, tint: 'dark' } : {}
return (
<ContentContainerView>
@@ -41,11 +38,11 @@ const Overlay = ({
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
type: "tween",
type: 'tween',
duration: 500,
}}
style={{
position: isNative ? "absolute" : "fixed",
position: isNative ? 'absolute' : 'fixed',
...StyleSheet.absoluteFillObject,
backgroundColor: Palette.ultraLightBlack,
flex: 1,
@@ -55,8 +52,8 @@ const Overlay = ({
<BackgroundView
{...backgroundProps}
style={{
width: "100%",
height: "100%",
width: '100%',
height: '100%',
...Style.containerCenter,
...StyleSheet.absoluteFillObject,
...contentContainerStyle,
@@ -70,8 +67,8 @@ const Overlay = ({
...StyleSheet.absoluteFillObject,
}}
onPress={() => {
console.log("overlay pressed");
setIsVisible?.(false);
console.log('overlay pressed')
setIsVisible?.(false)
}}
/>
{children}
@@ -80,7 +77,7 @@ const Overlay = ({
) : null}
</AnimatePresence>
</ContentContainerView>
);
};
)
}
export default Overlay;
export default Overlay
+10 -18
View File
@@ -1,33 +1,25 @@
import { View } from "react-native";
import { Motion } from "@legendapp/motion";
import { View } from 'react-native'
import { Motion } from '@legendapp/motion'
import { Palette } from "../styles";
import { Palette } from '../styles'
export default ({
carouselRef = null,
selectedIndex,
length,
containerStyle = {},
}) => {
export default ({ carouselRef = null, selectedIndex, length, containerStyle = {} }) => {
return (
<View style={{ flexDirection: "row", ...containerStyle }}>
<View style={{ flexDirection: 'row', ...containerStyle }}>
{Array.from({ length }).map((_, index) => (
<Motion.Pressable
key={index}
onPress={() => {
if (carouselRef?.current && index !== selectedIndex) {
console.log("scroll to", index);
carouselRef.current.scrollTo({ index });
console.log('scroll to', index)
carouselRef.current.scrollTo({ index })
}
}}
style={{
width: 10,
height: 10,
borderRadius: 5,
backgroundColor:
index === selectedIndex
? Palette.primary
: Palette.ultraLightWhite,
backgroundColor: index === selectedIndex ? Palette.primary : Palette.ultraLightWhite,
marginHorizontal: 5,
}}
animate={{
@@ -36,5 +28,5 @@ export default ({
/>
))}
</View>
);
};
)
}
+26 -42
View File
@@ -1,57 +1,49 @@
import React, { useGlobal } from "reactn";
import { View, Text, Pressable } from "react-native";
import { capitalize } from "lodash";
import moment from "moment";
import { Motion } from "@legendapp/motion";
import { validateDate } from "react-native-minuit/src/actions/dateActions";
import React, { useGlobal } from 'reactn'
import { View, Text, Pressable } from 'react-native'
import { capitalize } from 'lodash'
import moment from 'moment'
import { Motion } from '@legendapp/motion'
import { validateDate } from 'react-native-minuit/src/actions/dateActions'
import { Fonts, Palette, Style } from "../styles";
import { projectsRef } from "../config/firebase";
import { Fonts, Palette, Style } from '../styles'
import { projectsRef } from '../config/firebase'
export default ({ actionList = [] }) => {
const [currentProjectID] = useGlobal("currentProjectID");
const [currentProjectID] = useGlobal('currentProjectID')
const onCheck = async ({ todoID, isChecked = false }) => {
try {
const updatedObject = {
isChecked: !isChecked,
completionTimestamp: !isChecked ? new Date() : null,
};
}
await projectsRef
.doc(currentProjectID)
.collection("todoList")
.collection('todoList')
.doc(todoID)
.update(updatedObject);
.update(updatedObject)
} catch (error) {
console.log("error", error);
console.log('error', error)
}
};
}
if (!actionList.length) {
return (
<Text style={Fonts({ type: "default", style: {} })}>
<Text style={Fonts({ type: 'default', style: {} })}>
Vous n'avez pas d'actions en attente.
</Text>
);
)
}
return actionList.map(
(
{
title = "",
requestedCompletionTimestamp = null,
isChecked = false,
todoID = null,
},
{ title = '', requestedCompletionTimestamp = null, isChecked = false, todoID = null },
index
) => {
return (
<React.Fragment key={index}>
<Pressable
style={Style.containerRow}
onPress={() => onCheck({ todoID, isChecked })}
>
<Pressable style={Style.containerRow} onPress={() => onCheck({ todoID, isChecked })}>
<View style={[Style.containerRadio, { marginRight: 20 }]}>
{isChecked && (
<Motion.View
@@ -65,10 +57,8 @@ export default ({ actionList = [] }) => {
<View>
<Text
style={Fonts({
type: "default",
style: isChecked
? { textDecorationLine: "line-through" }
: {},
type: 'default',
style: isChecked ? { textDecorationLine: 'line-through' } : {},
})}
>
{capitalize(title)}
@@ -76,28 +66,22 @@ export default ({ actionList = [] }) => {
<Text
style={Fonts({
type: "default",
type: 'default',
color: Palette.transparentWhite,
})}
>
{capitalize(
moment(validateDate(requestedCompletionTimestamp)).fromNow()
)}
{capitalize(moment(validateDate(requestedCompletionTimestamp)).fromNow())}
</Text>
</View>
</Pressable>
{index !== actionList.length - 1 && (
<View
style={[
Style.separatorHorizontal,
Style.containerRevertGutter,
{ width: "112%" },
]}
style={[Style.separatorHorizontal, Style.containerRevertGutter, { width: '112%' }]}
/>
)}
</React.Fragment>
);
)
}
);
};
)
}
+19 -19
View File
@@ -1,11 +1,11 @@
import React, { useCallback, useEffect } from "react";
import { Pressable } from "react-native";
import React, { useCallback, useEffect } from 'react'
import { Pressable } from 'react-native'
import Animated, {
cancelAnimation,
useAnimatedStyle,
useSharedValue,
withSpring,
} from "react-native-reanimated";
} from 'react-native-reanimated'
export default function PressableScale({
children,
@@ -18,31 +18,31 @@ export default function PressableScale({
spring = { damping: 18, stiffness: 220, mass: 0.8 },
...rest
}) {
const sv = useSharedValue(1);
const sv = useSharedValue(1)
const rStyle = useAnimatedStyle(() => ({
transform: [{ scale: sv.value }],
}));
}))
const pressIn = useCallback(() => {
if (disabled) return;
cancelAnimation(sv);
sv.value = 1;
sv.value = withSpring(scaleTo, spring);
}, [disabled, scaleTo, spring, sv]);
if (disabled) return
cancelAnimation(sv)
sv.value = 1
sv.value = withSpring(scaleTo, spring)
}, [disabled, scaleTo, spring, sv])
const reset = useCallback(() => {
cancelAnimation(sv);
sv.value = withSpring(1, spring);
}, [spring, sv]);
cancelAnimation(sv)
sv.value = withSpring(1, spring)
}, [spring, sv])
useEffect(
() => () => {
cancelAnimation(sv);
sv.value = 1;
cancelAnimation(sv)
sv.value = 1
},
[sv]
);
)
return (
<Pressable
@@ -54,8 +54,8 @@ export default function PressableScale({
onMouseLeave={reset} // web
onBlur={reset}
onPress={(e) => {
onPress?.(e);
reset();
onPress?.(e)
reset()
}}
hitSlop={hitSlop}
disabled={disabled}
@@ -64,5 +64,5 @@ export default function PressableScale({
>
<Animated.View style={[contentStyle, rStyle]}>{children}</Animated.View>
</Pressable>
);
)
}
+16 -16
View File
@@ -1,20 +1,20 @@
import { Image } from "expo-image";
import { memo } from "react";
import { Platform, StyleSheet, View } from "react-native";
import { Palette } from "../styles";
import { Image } from 'expo-image'
import { memo } from 'react'
import { Platform, StyleSheet, View } from 'react-native'
import { Palette } from '../styles'
const DEFAULT_INTENSITY = Platform.select({ ios: 22, default: 14 });
const DEFAULT_INTENSITY = Platform.select({ ios: 22, default: 14 })
const ProfilePicture = ({
uri = null,
size = 48,
style,
blurIntensity = DEFAULT_INTENSITY,
blurTint = "dark",
blurTint = 'dark',
imageProps = {},
transitionDuration = 120,
cachePolicy = "memory-disk",
contentFit = "cover",
cachePolicy = 'memory-disk',
contentFit = 'cover',
accessibilityLabel,
...rest
}) => {
@@ -22,7 +22,7 @@ const ProfilePicture = ({
width: size,
height: size,
borderRadius: size / 2,
};
}
return (
<View
@@ -47,19 +47,19 @@ const ProfilePicture = ({
</>
)}
</View>
);
};
)
}
const styles = StyleSheet.create({
container: {
overflow: "hidden",
backgroundColor: "transparent",
overflow: 'hidden',
backgroundColor: 'transparent',
},
fallback: {
backgroundColor: "#7373736e",
backgroundColor: '#7373736e',
borderColor: Palette.ultraLightWhite,
borderWidth: 2,
},
});
})
export default memo(ProfilePicture);
export default memo(ProfilePicture)
+21 -21
View File
@@ -1,42 +1,42 @@
import { useEffect, useState } from "react";
import { View } from "react-native";
import { mainBorderRadius } from "../styles/Style";
import { Palette } from "../styles";
import { LinearGradient } from "./LinearGradient/LinearGradient";
import { useEffect, useState } from 'react'
import { View } from 'react-native'
import { mainBorderRadius } from '../styles/Style'
import { Palette } from '../styles'
import { LinearGradient } from './LinearGradient/LinearGradient'
const ProgressBar = ({
progress = 0,
containerStyle = {},
gradient = false,
status = "default",
status = 'default',
}) => {
const numericProgress = Number(progress);
const numericProgress = Number(progress)
const normalizedProgress = Math.max(
0,
Math.min(100, Number.isFinite(numericProgress) ? numericProgress : 0),
);
const isError = status === "error";
const containerBackground = isError ? "#3C101B" : "#0F0C19";
const fillColor = isError ? "#FF6B6B" : Palette.white;
const shouldUseGradient = gradient && !isError;
Math.min(100, Number.isFinite(numericProgress) ? numericProgress : 0)
)
const isError = status === 'error'
const containerBackground = isError ? '#3C101B' : '#0F0C19'
const fillColor = isError ? '#FF6B6B' : Palette.white
const shouldUseGradient = gradient && !isError
return (
<View
style={{
width: "100%",
width: '100%',
height: 5,
backgroundColor: containerBackground,
borderRadius: mainBorderRadius,
overflow: "hidden",
overflow: 'hidden',
...containerStyle,
}}
>
{shouldUseGradient ? (
<LinearGradient
colors={["#F94697", "#7023F7"]}
colors={['#F94697', '#7023F7']}
style={{
width: `${normalizedProgress}%`,
height: "100%",
height: '100%',
borderRadius: mainBorderRadius,
}}
start={{ x: 0, y: 0 }}
@@ -46,14 +46,14 @@ const ProgressBar = ({
<View
style={{
width: `${normalizedProgress}%`,
height: "100%",
height: '100%',
backgroundColor: fillColor,
borderRadius: mainBorderRadius,
}}
/>
)}
</View>
);
};
)
}
export default ProgressBar;
export default ProgressBar
+250 -291
View File
@@ -1,5 +1,5 @@
import { BlurView } from "expo-blur";
import { Portal } from "@gorhom/portal";
import { BlurView } from 'expo-blur'
import { Portal } from '@gorhom/portal'
import React, {
forwardRef,
useCallback,
@@ -8,7 +8,7 @@ import React, {
useMemo,
useRef,
useState,
} from "react";
} from 'react'
import {
Animated,
Dimensions,
@@ -20,35 +20,29 @@ import {
StyleSheet,
Text,
View,
} from "react-native";
import { icons, img } from "../../assets";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import GradientButton from "../GradientButton";
} from 'react-native'
import { icons, img } from '../../assets'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import GradientButton from '../GradientButton'
const BUTTON_BLUR_INTENSITY = 20;
const DROPDOWN_BACKGROUND_COLOR = "#252438";
const IS_WEB = Platform.OS === "web";
const BUTTON_BLUR_INTENSITY = 20
const DROPDOWN_BACKGROUND_COLOR = '#252438'
const IS_WEB = Platform.OS === 'web'
const WEB_BLUR_FALLBACK_STYLE = {
backgroundColor: "transparent",
};
backgroundColor: 'transparent',
}
const BLUR_VIEW_PROPS =
Platform.OS === "android"
? { experimentalBlurMethod: "dimezisBlurView" }
: {};
const toStyleArray = (style) =>
Array.isArray(style) ? style : style ? [style] : [];
Platform.OS === 'android' ? { experimentalBlurMethod: 'dimezisBlurView' } : {}
const toStyleArray = (style) => (Array.isArray(style) ? style : style ? [style] : [])
const ThemedBlurView = ({ style, children, ...rest }) => {
if (IS_WEB) {
return (
<View
{...rest}
style={[WEB_BLUR_FALLBACK_STYLE, ...toStyleArray(style)]}
>
<View {...rest} style={[WEB_BLUR_FALLBACK_STYLE, ...toStyleArray(style)]}>
{children}
</View>
);
)
}
return (
@@ -61,24 +55,24 @@ const ThemedBlurView = ({ style, children, ...rest }) => {
>
{children}
</BlurView>
);
};
)
}
const defaultFormatDate = (timestamp) => {
try {
const value = timestamp?.toDate ? timestamp.toDate() : timestamp;
const date = value ? new Date(value) : null;
const value = timestamp?.toDate ? timestamp.toDate() : timestamp
const date = value ? new Date(value) : null
if (!date || Number.isNaN(date.getTime())) {
return "";
return ''
}
const day = date.getDate().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0");
return `${day}/${month}`;
const day = date.getDate().toString().padStart(2, '0')
const month = (date.getMonth() + 1).toString().padStart(2, '0')
return `${day}/${month}`
} catch (error) {
console.warn("ProjectDropDown: unable to format date", error);
return "";
console.warn('ProjectDropDown: unable to format date', error)
return ''
}
};
}
const ProjectDropDown = forwardRef(
(
@@ -94,62 +88,61 @@ const ProjectDropDown = forwardRef(
},
ref
) => {
const [isOpen, setIsOpen] = useState(false);
const [triggerLayout, setTriggerLayout] = useState(null);
const [triggerWindowLayout, setTriggerWindowLayout] = useState(null);
const triggerRef = useRef(null);
const dropdownAnimation = useRef(new Animated.Value(0)).current;
const [isOpen, setIsOpen] = useState(false)
const [triggerLayout, setTriggerLayout] = useState(null)
const [triggerWindowLayout, setTriggerWindowLayout] = useState(null)
const triggerRef = useRef(null)
const dropdownAnimation = useRef(new Animated.Value(0)).current
const measureTriggerPosition = useCallback(() => {
if (triggerRef.current?.measureInWindow) {
try {
triggerRef.current.measureInWindow((x, y, width, height) => {
setTriggerWindowLayout({
x: x ?? 0,
y: y ?? 0,
width: width ?? triggerLayout?.width ?? 0,
height: height ?? triggerLayout?.height ?? 0,
});
});
} catch (error) {
console.warn("ProjectDropDown: measureInWindow failed", error);
}
}
}, [triggerLayout?.height, triggerLayout?.width]);
const normalizedProjects = Array.isArray(projects) ? projects : [];
const isDisabled = normalizedProjects.length === 0;
const hasExplicitNullSelection =
allowEmptySelection && selectedProject === null;
const currentProject = hasExplicitNullSelection
? null
: selectedProject?.id
? selectedProject
: normalizedProjects[0] || null;
const dropdownProjects = currentProject
? normalizedProjects.filter((project) => {
if (!project) return false;
if (currentProject.id && project.id) {
return project.id !== currentProject.id;
const measureTriggerPosition = useCallback(() => {
if (triggerRef.current?.measureInWindow) {
try {
triggerRef.current.measureInWindow((x, y, width, height) => {
setTriggerWindowLayout({
x: x ?? 0,
y: y ?? 0,
width: width ?? triggerLayout?.width ?? 0,
height: height ?? triggerLayout?.height ?? 0,
})
})
} catch (error) {
console.warn('ProjectDropDown: measureInWindow failed', error)
}
return project !== currentProject;
})
: normalizedProjects;
}
}, [triggerLayout?.height, triggerLayout?.width])
const normalizedProjects = Array.isArray(projects) ? projects : []
const isDisabled = normalizedProjects.length === 0
const hasExplicitNullSelection = allowEmptySelection && selectedProject === null
const currentProject = hasExplicitNullSelection
? null
: selectedProject?.id
? selectedProject
: normalizedProjects[0] || null
const dropdownProjects = currentProject
? normalizedProjects.filter((project) => {
if (!project) return false
if (currentProject.id && project.id) {
return project.id !== currentProject.id
}
return project !== currentProject
})
: normalizedProjects
const toggleDropdown = useCallback(() => {
if (isDisabled) {
return;
return
}
if (!isOpen) {
measureTriggerPosition();
measureTriggerPosition()
}
setIsOpen((prev) => !prev);
}, [isDisabled, isOpen, measureTriggerPosition]);
setIsOpen((prev) => !prev)
}, [isDisabled, isOpen, measureTriggerPosition])
const closeDropdown = useCallback(() => setIsOpen(false), []);
const closeDropdown = useCallback(() => setIsOpen(false), [])
useImperativeHandle(
ref,
@@ -157,148 +150,140 @@ const ProjectDropDown = forwardRef(
close: closeDropdown,
}),
[closeDropdown]
);
)
const handleSelect = useCallback(
(project) => {
if (!project?.id) return;
onSelectProject?.(project);
closeDropdown();
if (!project?.id) return
onSelectProject?.(project)
closeDropdown()
},
[closeDropdown, onSelectProject]
);
)
const handleModify = useCallback(
(project, anchor) => {
if (!project?.id) return;
onModifyProject?.(project, anchor);
if (!project?.id) return
onModifyProject?.(project, anchor)
},
[onModifyProject]
);
)
const handleCreate = useCallback(() => {
onCreateProject?.();
closeDropdown();
}, [closeDropdown, onCreateProject]);
onCreateProject?.()
closeDropdown()
}, [closeDropdown, onCreateProject])
const dropdownWidth = useMemo(
() => triggerLayout?.width || 0,
[triggerLayout]
);
const dropdownWidth = useMemo(() => triggerLayout?.width || 0, [triggerLayout])
useEffect(() => {
Animated.timing(dropdownAnimation, {
toValue: isOpen ? 1 : 0,
duration: 220,
easing: isOpen ? Easing.out(Easing.cubic) : Easing.in(Easing.cubic),
useNativeDriver: true,
}).start();
}, [dropdownAnimation, isOpen]);
useEffect(() => {
Animated.timing(dropdownAnimation, {
toValue: isOpen ? 1 : 0,
duration: 220,
easing: isOpen ? Easing.out(Easing.cubic) : Easing.in(Easing.cubic),
useNativeDriver: true,
}).start()
}, [dropdownAnimation, isOpen])
useEffect(() => {
if (isOpen) {
measureTriggerPosition();
}
}, [isOpen, measureTriggerPosition]);
useEffect(() => {
if (isOpen) {
measureTriggerPosition()
}
}, [isOpen, measureTriggerPosition])
const overlayTranslateY = useMemo(
() =>
dropdownAnimation.interpolate({
inputRange: [0, 1],
outputRange: [-12, 0],
}),
[dropdownAnimation]
);
const overlayTranslateY = useMemo(
() =>
dropdownAnimation.interpolate({
inputRange: [0, 1],
outputRange: [-12, 0],
}),
[dropdownAnimation]
)
const overlayScale = useMemo(
() =>
dropdownAnimation.interpolate({
inputRange: [0, 1],
outputRange: [0.96, 1],
}),
[dropdownAnimation]
);
const overlayScale = useMemo(
() =>
dropdownAnimation.interpolate({
inputRange: [0, 1],
outputRange: [0.96, 1],
}),
[dropdownAnimation]
)
const triggerOpacity = useMemo(
() =>
dropdownAnimation.interpolate({
inputRange: [0, 1],
outputRange: [1, 0],
extrapolate: "clamp",
}),
[dropdownAnimation]
);
const triggerOpacity = useMemo(
() =>
dropdownAnimation.interpolate({
inputRange: [0, 1],
outputRange: [1, 0],
extrapolate: 'clamp',
}),
[dropdownAnimation]
)
const triggerTranslateY = useMemo(
() =>
dropdownAnimation.interpolate({
inputRange: [0, 1],
outputRange: [0, -6],
}),
[dropdownAnimation]
);
const triggerTranslateY = useMemo(
() =>
dropdownAnimation.interpolate({
inputRange: [0, 1],
outputRange: [0, -6],
}),
[dropdownAnimation]
)
const overlayPositionStyle = useMemo(() => {
const widthCandidate =
dropdownWidth ||
triggerWindowLayout?.width ||
triggerLayout?.width ||
0;
const overlayPositionStyle = useMemo(() => {
const widthCandidate =
dropdownWidth || triggerWindowLayout?.width || triggerLayout?.width || 0
const width = widthCandidate || 0;
const windowWidth = Dimensions.get("window").width || 0;
const leftBase = triggerWindowLayout?.x ?? 0;
const left =
width > 0 && windowWidth > 0
? Math.min(Math.max(leftBase, 0), Math.max(0, windowWidth - width))
: Math.max(leftBase, 0);
const width = widthCandidate || 0
const windowWidth = Dimensions.get('window').width || 0
const leftBase = triggerWindowLayout?.x ?? 0
const left =
width > 0 && windowWidth > 0
? Math.min(Math.max(leftBase, 0), Math.max(0, windowWidth - width))
: Math.max(leftBase, 0)
return {
top: triggerWindowLayout?.y ?? 0,
left,
width: width || undefined,
};
}, [dropdownWidth, triggerLayout?.width, triggerWindowLayout]);
return {
top: triggerWindowLayout?.y ?? 0,
left,
width: width || undefined,
}
}, [dropdownWidth, triggerLayout?.width, triggerWindowLayout])
const dropdownContent = (
<ThemedBlurView style={styles.dropdownOverlay}>
<ProjectRow
isTitle={true}
isOpen={isOpen}
project={currentProject}
formatDate={formatDate}
onSelect={toggleDropdown}
/>
<View style={styles.dropdownOverlayContent}>
<FlatList
data={dropdownProjects}
keyExtractor={(project, index) =>
project?.id || project?.title || `project-${index}`
}
ItemSeparatorComponent={() => <View style={{ height: 10 }} />}
renderItem={({ item: project }) => (
<ProjectRow
project={project}
formatDate={formatDate}
isSelected={currentProject?.id === project?.id}
onSelect={handleSelect}
onModify={handleModify}
/>
)}
nestedScrollEnabled
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.dropdownListContent}
style={styles.dropdownList}
const dropdownContent = (
<ThemedBlurView style={styles.dropdownOverlay}>
<ProjectRow
isTitle={true}
isOpen={isOpen}
project={currentProject}
formatDate={formatDate}
onSelect={toggleDropdown}
/>
<GradientButton
containerStyle={styles.createButtonContainer}
title="Commencer à créer"
onPress={handleCreate}
/>
</View>
</ThemedBlurView>
);
<View style={styles.dropdownOverlayContent}>
<FlatList
data={dropdownProjects}
keyExtractor={(project, index) => project?.id || project?.title || `project-${index}`}
ItemSeparatorComponent={() => <View style={{ height: 10 }} />}
renderItem={({ item: project }) => (
<ProjectRow
project={project}
formatDate={formatDate}
isSelected={currentProject?.id === project?.id}
onSelect={handleSelect}
onModify={handleModify}
/>
)}
nestedScrollEnabled
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.dropdownListContent}
style={styles.dropdownList}
/>
<GradientButton
containerStyle={styles.createButtonContainer}
title="Commencer à créer"
onPress={handleCreate}
/>
</View>
</ThemedBlurView>
)
return (
<View style={[styles.container, style]}>
@@ -306,12 +291,12 @@ const ProjectDropDown = forwardRef(
ref={triggerRef}
style={styles.triggerWrapper}
onLayout={(event) => {
setTriggerLayout(event.nativeEvent.layout);
measureTriggerPosition();
setTriggerLayout(event.nativeEvent.layout)
measureTriggerPosition()
}}
>
<Animated.View
pointerEvents={isOpen ? "none" : "auto"}
pointerEvents={isOpen ? 'none' : 'auto'}
style={[
styles.dropdownTriggerContainer,
{
@@ -321,10 +306,7 @@ const ProjectDropDown = forwardRef(
]}
>
<ThemedBlurView
style={[
styles.dropdownBlur,
isDisabled && styles.dropdownBlurDisabled,
]}
style={[styles.dropdownBlur, isDisabled && styles.dropdownBlurDisabled]}
>
<ProjectRow
isTitle={true}
@@ -340,10 +322,7 @@ const ProjectDropDown = forwardRef(
<Portal>
<View style={styles.portalContainer}>
<Pressable style={styles.backdrop} onPress={closeDropdown}>
<ThemedBlurView
pointerEvents="none"
style={styles.backdropBlur}
/>
<ThemedBlurView pointerEvents="none" style={styles.backdropBlur} />
</Pressable>
<Animated.View
pointerEvents="auto"
@@ -352,10 +331,7 @@ const ProjectDropDown = forwardRef(
overlayPositionStyle,
{
opacity: dropdownAnimation,
transform: [
{ translateY: overlayTranslateY },
{ scale: overlayScale },
],
transform: [{ translateY: overlayTranslateY }, { scale: overlayScale }],
},
]}
>
@@ -366,81 +342,68 @@ const ProjectDropDown = forwardRef(
) : null}
</View>
</View>
);
)
}
);
)
export default ProjectDropDown;
export default ProjectDropDown
const ProjectRow = ({
project,
formatDate,
onSelect,
onModify,
isTitle = false,
isOpen,
}) => {
const rowRef = useRef(null);
const [layout, setLayout] = useState(null);
const ProjectRow = ({ project, formatDate, onSelect, onModify, isTitle = false, isOpen }) => {
const rowRef = useRef(null)
const [layout, setLayout] = useState(null)
const [menuPos, setMenuPos] = useState({
top: 0,
right: 0,
left: 0,
width: 0,
height: 0,
});
})
useEffect(() => {
if (!layout) return;
const top = (layout?.y || 0) + (layout?.height || 45);
setMenuPos((prev) => ({ ...prev, top }));
}, [layout]);
if (!layout) return
const top = (layout?.y || 0) + (layout?.height || 45)
setMenuPos((prev) => ({ ...prev, top }))
}, [layout])
const onPressMenu = () => {
if (!project?.id) return;
if (!project?.id) return
const updateAnchor = (x = 0, y = 0, width = 0, height = 0) => {
const windowWidth = Dimensions.get("window").width || 0;
const top = (y || 0) + (height || 0) + 8;
const left = Math.max(0, x || 0);
const right = Math.max(0, windowWidth - (left + (width || 0)));
const anchor = { top, right, left, width, height };
setMenuPos(anchor);
onModify?.(project, anchor);
};
const windowWidth = Dimensions.get('window').width || 0
const top = (y || 0) + (height || 0) + 8
const left = Math.max(0, x || 0)
const right = Math.max(0, windowWidth - (left + (width || 0)))
const anchor = { top, right, left, width, height }
setMenuPos(anchor)
onModify?.(project, anchor)
}
if (rowRef?.current?.measureInWindow) {
try {
rowRef.current.measureInWindow((x, y, width, height) => {
updateAnchor(x, y, width, height);
});
return;
updateAnchor(x, y, width, height)
})
return
} catch (_error) {
// Fallback to layout-derived position
}
}
onModify?.(project, menuPos);
};
const isEmptyProject = !project;
onModify?.(project, menuPos)
}
const isEmptyProject = !project
const coverUri =
project?.coverUrl ||
project?.coverUri ||
(typeof project?.cover === "string" ? project.cover : project?.cover?.uri);
const imageSource = useMemo(
() => (coverUri ? { uri: coverUri } : img.placeholder),
[coverUri]
);
const title =
project?.title || (isEmptyProject ? "Nouvelle musique" : "Sans titre");
const formattedDate = !isEmptyProject
? formatDate(project?.updatedAt || project?.createdAt)
: "";
(typeof project?.cover === 'string' ? project.cover : project?.cover?.uri)
const imageSource = useMemo(() => (coverUri ? { uri: coverUri } : img.placeholder), [coverUri])
const title = project?.title || (isEmptyProject ? 'Nouvelle musique' : 'Sans titre')
const formattedDate = !isEmptyProject ? formatDate(project?.updatedAt || project?.createdAt) : ''
const subtitle = isEmptyProject
? "Commencer une nouvelle musique"
? 'Commencer une nouvelle musique'
: formattedDate
? `Modifié le ${formattedDate}`
: "MusicLand";
: 'MusicLand'
return (
<Pressable
@@ -469,57 +432,53 @@ const ProjectRow = ({
<Pressable
hitSlop={8}
onPress={(event) => {
event.stopPropagation?.();
onPressMenu();
event.stopPropagation?.()
onPressMenu()
}}
>
<Image
source={icons.threeDots}
style={styles.moreIcon}
resizeMode="contain"
/>
<Image source={icons.threeDots} style={styles.moreIcon} resizeMode="contain" />
</Pressable>
)}
</ThemedBlurView>
</Pressable>
);
};
)
}
const styles = StyleSheet.create({
container: {
maxWidth: 450,
position: "relative",
position: 'relative',
},
dropdownBlur: {
borderRadius: 15,
padding: 8,
position: "relative",
overflow: "hidden",
position: 'relative',
overflow: 'hidden',
backgroundColor: DROPDOWN_BACKGROUND_COLOR,
},
dropdownBlurDisabled: {
opacity: 0.6,
},
triggerWrapper: {
position: "relative",
position: 'relative',
zIndex: 3,
},
dropdownTriggerContainer: {
width: "100%",
width: '100%',
},
dropdownOverlayContainer: {
position: "absolute",
position: 'absolute',
top: 0,
left: 0,
borderRadius: 16,
overflow: "hidden",
overflow: 'hidden',
zIndex: 4,
elevation: 4,
maxHeight: 400,
},
dropdownOverlay: {
flex: 1,
width: "100%",
width: '100%',
paddingHorizontal: 8,
paddingVertical: 8,
gap: 9,
@@ -528,28 +487,28 @@ const styles = StyleSheet.create({
},
dropdownOverlayContent: {
maxHeight: 260,
width: "100%",
width: '100%',
gap: 12,
},
dropdownList: {
maxHeight: 220,
width: "100%",
width: '100%',
},
dropdownListContent: {
paddingBottom: 4,
},
createButtonContainer: {
width: 200,
alignSelf: "center",
alignSelf: 'center',
},
chevron: {
width: 18,
height: 18,
tintColor: Palette.white,
transform: [{ rotate: "0deg" }],
transform: [{ rotate: '0deg' }],
},
chevronOpen: {
transform: [{ rotate: "180deg" }],
transform: [{ rotate: '180deg' }],
},
portalContainer: {
...StyleSheet.absoluteFillObject,
@@ -559,25 +518,25 @@ const styles = StyleSheet.create({
backdrop: {
...StyleSheet.absoluteFillObject,
zIndex: 1,
backgroundColor: "transparent",
backgroundColor: 'transparent',
},
backdropBlur: {
...StyleSheet.absoluteFillObject,
},
projectRowPressable: {
flexDirection: "row",
alignItems: "center",
flexDirection: 'row',
alignItems: 'center',
gap: 5,
width: "100%",
width: '100%',
},
projectInfo: {
flex: 1,
flexDirection: "row",
alignItems: "center",
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 18,
paddingVertical: 12,
borderRadius: 10,
overflow: "hidden",
overflow: 'hidden',
backgroundColor: DROPDOWN_BACKGROUND_COLOR,
},
projectImage: {
@@ -596,13 +555,13 @@ const styles = StyleSheet.create({
},
projectSubtitle: {
fontSize: 13,
color: "rgba(255, 255, 255, 0.7)",
color: 'rgba(255, 255, 255, 0.7)',
fontFamily: FONT_FAMILY.InterRegular,
},
moreIcon: {
width: 18,
height: 18,
tintColor: Palette.white,
transform: [{ rotate: "90deg" }],
transform: [{ rotate: '90deg' }],
},
});
})
+44 -51
View File
@@ -1,56 +1,49 @@
import { Text, View } from "react-native";
import { Image as ExpoImage } from "expo-image";
import { responsiveWidth } from "../actions/responsiveSizes.js";
import { getInitials } from "../helpers";
import { Fonts, Style } from "../styles";
import { Text, View } from 'react-native'
import { Image as ExpoImage } from 'expo-image'
import { responsiveWidth } from '../actions/responsiveSizes.js'
import { getInitials } from '../helpers'
import { Fonts, Style } from '../styles'
export default ({
name = "",
url = null,
size = responsiveWidth(7),
containerStyle = {},
}) => {
export default ({ name = '', url = null, size = responsiveWidth(7), containerStyle = {} }) => {
const colors = [
"#FB68A8",
"#FA6DA8",
"#F971A7",
"#F876A7",
"#F67BA6",
"#F580A6",
"#F384A5",
"#F289A5",
"#F08EA4",
"#EF93A4",
"#ED97A3",
"#EC9CA3",
"#EAA1A2",
"#E8A6A2",
"#E7AAA1",
"#E5AFA1",
"#E3B4A0",
"#E2B9A0",
"#E0BDAF",
"#DFC2AE",
"#DDC7AE",
"#DBCBAE",
"#DACFAE",
"#D8D4AE",
"#D6D8AE",
"#D5DCAE",
"#D3E1AE",
"#D2E5AE",
"#D0E9AE",
"#CFEDAE",
];
'#FB68A8',
'#FA6DA8',
'#F971A7',
'#F876A7',
'#F67BA6',
'#F580A6',
'#F384A5',
'#F289A5',
'#F08EA4',
'#EF93A4',
'#ED97A3',
'#EC9CA3',
'#EAA1A2',
'#E8A6A2',
'#E7AAA1',
'#E5AFA1',
'#E3B4A0',
'#E2B9A0',
'#E0BDAF',
'#DFC2AE',
'#DDC7AE',
'#DBCBAE',
'#DACFAE',
'#D8D4AE',
'#D6D8AE',
'#D5DCAE',
'#D3E1AE',
'#D2E5AE',
'#D0E9AE',
'#CFEDAE',
]
const pickColor = (name) => {
const sumChars = name
.split("")
.reduce((acc, char) => acc + char.charCodeAt(0), 0);
const index = sumChars % 30;
const sumChars = name.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)
const index = sumChars % 30
return colors[index];
};
return colors[index]
}
return (
<View
@@ -76,10 +69,10 @@ export default ({
}}
/>
) : (
<Text style={Fonts({ type: "section", style: { fontSize: size / 3 } })}>
<Text style={Fonts({ type: 'section', style: { fontSize: size / 3 } })}>
{getInitials(name)}
</Text>
)}
</View>
);
};
)
}
+22 -22
View File
@@ -1,32 +1,32 @@
import React, { useMemo } from "react";
import { View } from "react-native";
import Svg, { Rect } from "react-native-svg";
import { generateQrMatrix } from "../utils/generateQrMatrix";
import React, { useMemo } from 'react'
import { View } from 'react-native'
import Svg, { Rect } from 'react-native-svg'
import { generateQrMatrix } from '../utils/generateQrMatrix'
const DEFAULT_SIZE = 220;
const DEFAULT_SIZE = 220
const QrCode = ({
value,
size = DEFAULT_SIZE,
color = "#FFFFFF",
backgroundColor = "transparent",
errorCorrectionLevel = "M",
color = '#FFFFFF',
backgroundColor = 'transparent',
errorCorrectionLevel = 'M',
}) => {
const { matrix } = useMemo(() => {
try {
return generateQrMatrix({ value, errorCorrectionLevel });
return generateQrMatrix({ value, errorCorrectionLevel })
} catch (error) {
console.error("qr.code.generate.error", error);
return { matrix: [] };
console.error('qr.code.generate.error', error)
return { matrix: [] }
}
}, [value, errorCorrectionLevel]);
}, [value, errorCorrectionLevel])
const cellSize = useMemo(() => {
if (!matrix || !matrix.length) {
return 0;
return 0
}
return size / matrix.length;
}, [matrix, size]);
return size / matrix.length
}, [matrix, size])
return (
<View
@@ -40,10 +40,10 @@ const QrCode = ({
{matrix.map((row, rowIndex) => {
return row.map((isDark, colIndex) => {
if (!isDark) {
return null;
return null
}
const key = `${rowIndex}-${colIndex}`;
const key = `${rowIndex}-${colIndex}`
return (
<Rect
@@ -54,12 +54,12 @@ const QrCode = ({
height={cellSize}
fill={color}
/>
);
});
)
})
})}
</Svg>
</View>
);
};
)
}
export default QrCode;
export default QrCode
+21 -24
View File
@@ -1,24 +1,21 @@
import { useContext } from "reactn";
import { Pressable, View, Image, Text } from "react-native";
import { Video, ResizeMode } from "expo-video";
import { Fonts, gutters, Palette } from "../styles";
import Style, { mainBorderRadius } from "../styles/Style";
import { icons } from "../assets";
import { substringTextLength } from "../helpers";
import { WebViewContext } from "../providers/WebViewProvider.js";
import { useContext } from 'reactn'
import { Pressable, View, Image, Text } from 'react-native'
import { Video, ResizeMode } from 'expo-video'
import { Fonts, gutters, Palette } from '../styles'
import Style, { mainBorderRadius } from '../styles/Style'
import { icons } from '../assets'
import { substringTextLength } from '../helpers'
import { WebViewContext } from '../providers/WebViewProvider.js'
const thumbnailStyle = {
width: 200,
height: 200,
borderRadius: mainBorderRadius / 2,
overflow: "hidden",
};
overflow: 'hidden',
}
const RenderChatFile = (
{ uri, type = "image", name = "", containerStyle = {} },
index,
) => {
const { setWebViewUrl } = useContext(WebViewContext);
const RenderChatFile = ({ uri, type = 'image', name = '', containerStyle = {} }, index) => {
const { setWebViewUrl } = useContext(WebViewContext)
return (
<View
@@ -28,7 +25,7 @@ const RenderChatFile = (
...containerStyle,
}}
>
{type === "IMAGE" ? (
{type === 'IMAGE' ? (
<Pressable>
<Image
alt={name}
@@ -38,10 +35,10 @@ const RenderChatFile = (
style={thumbnailStyle}
/>
</Pressable>
) : type === "FILE" ? (
) : type === 'FILE' ? (
<Pressable
onPress={() => {
setWebViewUrl(uri);
setWebViewUrl(uri)
}}
style={{
...Style.containerRow,
@@ -61,17 +58,17 @@ const RenderChatFile = (
/>
<Text
style={Fonts({
type: "default",
type: 'default',
style: {
color: Palette.white,
textDecorationLine: "underline",
textDecorationLine: 'underline',
},
})}
>
{substringTextLength({ text: name, maxLength: 40 })}
</Text>
</Pressable>
) : type === "VIDEO" ? (
) : type === 'VIDEO' ? (
<Video
style={{ ...thumbnailStyle }}
source={{
@@ -88,7 +85,7 @@ const RenderChatFile = (
/>
) : null}
</View>
);
};
)
}
export default RenderChatFile;
export default RenderChatFile
+22 -22
View File
@@ -1,6 +1,6 @@
import React from "react";
import React from 'react'
import "./rotationTestStyle.css";
import './rotationTestStyle.css'
// Web-only rotating gradient border container
// Props:
@@ -16,44 +16,44 @@ const RotationBorder = ({
style,
className,
active = true,
colors = ["#F94697", "#7023F7"],
colors = ['#F94697', '#7023F7'],
duration = 5,
borderWidth = 1,
borderRadius,
fillColor = "transparent",
fillColor = 'transparent',
}) => {
const classes = [
"rotation-border",
active ? "rotation-border--active" : "rotation-border--static",
'rotation-border',
active ? 'rotation-border--active' : 'rotation-border--static',
className,
]
.filter(Boolean)
.join(" ");
.join(' ')
const inlineStyle = { ...style };
const inlineStyle = { ...style }
inlineStyle["--rotation-border-width"] = `${borderWidth}px`;
inlineStyle["--rotation-border-color-1"] = colors[0];
inlineStyle["--rotation-border-color-2"] = colors[1] ?? colors[0];
inlineStyle["--rotation-border-duration"] = `${duration}s`;
inlineStyle["--rotation-border-fill"] = fillColor;
inlineStyle['--rotation-border-width'] = `${borderWidth}px`
inlineStyle['--rotation-border-color-1'] = colors[0]
inlineStyle['--rotation-border-color-2'] = colors[1] ?? colors[0]
inlineStyle['--rotation-border-duration'] = `${duration}s`
inlineStyle['--rotation-border-fill'] = fillColor
if (typeof borderRadius !== "undefined") {
inlineStyle.borderRadius = borderRadius;
if (typeof borderRadius !== 'undefined') {
inlineStyle.borderRadius = borderRadius
}
if (typeof inlineStyle.borderRadius !== "undefined") {
inlineStyle["--rotation-border-radius"] =
typeof inlineStyle.borderRadius === "number"
if (typeof inlineStyle.borderRadius !== 'undefined') {
inlineStyle['--rotation-border-radius'] =
typeof inlineStyle.borderRadius === 'number'
? `${inlineStyle.borderRadius}px`
: inlineStyle.borderRadius;
: inlineStyle.borderRadius
}
return (
<div className={classes} style={inlineStyle}>
{children}
</div>
);
};
)
}
export default RotationBorder;
export default RotationBorder
+50 -60
View File
@@ -1,108 +1,98 @@
import { BlurView } from "expo-blur";
import React, { useCallback, useEffect, useRef } from "react";
import { Image, Pressable, TextInput } from "react-native";
import { icons } from "../assets";
import { Palette } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import Style from "../styles/Style";
import { BlurView } from 'expo-blur'
import React, { useCallback, useEffect, useRef } from 'react'
import { Image, Pressable, TextInput } from 'react-native'
import { icons } from '../assets'
import { Palette } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
import Style from '../styles/Style'
const SearchBar = ({
placeholder = "Que souhaites-tu écouter?",
textInputProps = {},
}) => {
const inputRef = useRef(null);
const hasForwardedFocusRef = useRef(false);
const focusFallbackRef = useRef(null);
const SearchBar = ({ placeholder = 'Que souhaites-tu écouter?', textInputProps = {} }) => {
const inputRef = useRef(null)
const hasForwardedFocusRef = useRef(false)
const focusFallbackRef = useRef(null)
const {
onFocus: onFocusProp,
onPressIn: onPressInProp,
ref: textInputRefProp,
...restTextInputProps
} = textInputProps;
} = textInputProps
const forwardFocus = useCallback(
(event) => {
if (hasForwardedFocusRef.current) return;
hasForwardedFocusRef.current = true;
onFocusProp?.(event);
if (hasForwardedFocusRef.current) return
hasForwardedFocusRef.current = true
onFocusProp?.(event)
},
[onFocusProp],
);
[onFocusProp]
)
const handleFocus = useCallback(
(event) => {
forwardFocus(event);
forwardFocus(event)
},
[forwardFocus],
);
[forwardFocus]
)
const focusInput = useCallback(() => {
const node = inputRef.current;
const node = inputRef.current
if (!node) {
return;
return
}
if (
typeof node.isFocused === "function" &&
node.isFocused() &&
hasForwardedFocusRef.current
) {
return;
if (typeof node.isFocused === 'function' && node.isFocused() && hasForwardedFocusRef.current) {
return
}
hasForwardedFocusRef.current = false;
node.focus?.();
hasForwardedFocusRef.current = false
node.focus?.()
if (focusFallbackRef.current) {
cancelAnimationFrame(focusFallbackRef.current);
cancelAnimationFrame(focusFallbackRef.current)
}
focusFallbackRef.current = requestAnimationFrame(() => {
// Some platforms do not propagate the focus event when focus() is called programmatically.
if (!hasForwardedFocusRef.current) {
forwardFocus();
forwardFocus()
}
if (typeof node.isFocused === "function" && !node.isFocused()) {
node.focus?.();
if (typeof node.isFocused === 'function' && !node.isFocused()) {
node.focus?.()
}
});
}, [forwardFocus]);
})
}, [forwardFocus])
useEffect(() => {
return () => {
if (focusFallbackRef.current) {
cancelAnimationFrame(focusFallbackRef.current);
cancelAnimationFrame(focusFallbackRef.current)
}
};
}, []);
}
}, [])
const setRefs = useCallback(
(node) => {
inputRef.current = node;
inputRef.current = node
if (typeof textInputRefProp === "function") {
textInputRefProp(node);
if (typeof textInputRefProp === 'function') {
textInputRefProp(node)
} else if (textInputRefProp) {
textInputRefProp.current = node;
textInputRefProp.current = node
}
},
[textInputRefProp],
);
[textInputRefProp]
)
const handleInputPressIn = useCallback(
(event) => {
focusInput();
onPressInProp?.(event);
focusInput()
onPressInProp?.(event)
},
[focusInput, onPressInProp],
);
[focusInput, onPressInProp]
)
return (
<Pressable
onPressIn={focusInput}
style={{ borderRadius: 12, overflow: "hidden" }}
>
<Pressable onPressIn={focusInput} style={{ borderRadius: 12, overflow: 'hidden' }}>
<BlurView
intensity={80}
style={{
@@ -110,7 +100,7 @@ const SearchBar = ({
paddingHorizontal: 8,
gap: 10,
...Style.containerRow,
backgroundColor: "Palette.ultraLightBlack",
backgroundColor: 'Palette.ultraLightBlack',
}}
>
<Image source={icons.search} />
@@ -130,7 +120,7 @@ const SearchBar = ({
/>
</BlurView>
</Pressable>
);
};
)
}
export default SearchBar;
export default SearchBar
+13 -15
View File
@@ -1,24 +1,22 @@
import React from "reactn";
import { Text, View } from "react-native";
import React from 'reactn'
import { Text, View } from 'react-native'
import { Fonts, gutters, Style } from "../styles";
import { Fonts, gutters, Style } from '../styles'
const SectionTextHeader = ({
title = "",
rightText = "",
title = '',
rightText = '',
rightTextStyle = {},
subTitle = "",
subTitle = '',
containerStyle = {},
}) => {
return (
<View style={{ ...containerStyle }}>
<View
style={{ ...Style.containerSpaceBetween, marginBottom: gutters / 2 }}
>
<View style={{ ...Style.containerSpaceBetween, marginBottom: gutters / 2 }}>
<Text
style={{
...Fonts({
type: "section",
type: 'section',
}),
}}
>
@@ -28,7 +26,7 @@ const SectionTextHeader = ({
<Text
style={{
...Fonts({
type: "section",
type: 'section',
}),
...rightTextStyle,
}}
@@ -40,7 +38,7 @@ const SectionTextHeader = ({
{subTitle?.length > 0 && (
<Text
style={{
...Fonts({ type: "default", style: { opacity: 0.5 } }),
...Fonts({ type: 'default', style: { opacity: 0.5 } }),
marginBottom: gutters / 2,
}}
>
@@ -48,7 +46,7 @@ const SectionTextHeader = ({
</Text>
)}
</View>
);
};
)
}
export default SectionTextHeader;
export default SectionTextHeader
+20 -27
View File
@@ -1,24 +1,19 @@
import React, { useCallback } from "react";
import { BlurView } from "expo-blur";
import { Pressable, Text } from "react-native";
import { Entypo } from "@expo/vector-icons";
import { FONT_FAMILY } from "../../styles/Fonts";
import { openShareSheet } from "../../utils/shareSheet";
import React, { useCallback } from 'react'
import { BlurView } from 'expo-blur'
import { Pressable, Text } from 'react-native'
import { Entypo } from '@expo/vector-icons'
import { FONT_FAMILY } from '../../styles/Fonts'
import { openShareSheet } from '../../utils/shareSheet'
export default function ShareBtn({
style,
onPress = null,
label = null,
iconOnly = false,
}) {
export default function ShareBtn({ style, onPress = null, label = null, iconOnly = false }) {
const handlePress = useCallback(() => {
if (typeof onPress === "function") {
onPress();
return;
if (typeof onPress === 'function') {
onPress()
return
}
openShareSheet();
}, [onPress]);
openShareSheet()
}, [onPress])
return (
<Pressable
@@ -34,22 +29,20 @@ export default function ShareBtn({
padding: iconOnly ? 10 : label ? 10 : 12,
paddingHorizontal: iconOnly ? 10 : label ? 16 : 12,
borderRadius: 15,
overflow: "hidden",
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
overflow: 'hidden',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderWidth: iconOnly ? 1 : 0,
borderColor: iconOnly ? "rgba(255, 255, 255, 0.1)" : "transparent",
backgroundColor: iconOnly
? "rgba(12, 14, 18, 0.72)"
: "transparent",
borderColor: iconOnly ? 'rgba(255, 255, 255, 0.1)' : 'transparent',
backgroundColor: iconOnly ? 'rgba(12, 14, 18, 0.72)' : 'transparent',
}}
>
{label && !iconOnly ? (
<Text
style={{
fontSize: 14,
color: "#ffffff",
color: '#ffffff',
fontFamily: FONT_FAMILY.InterMedium,
marginRight: 8,
}}
@@ -61,5 +54,5 @@ export default function ShareBtn({
<Entypo name="share-alternative" size={18} color="white" />
</BlurView>
</Pressable>
);
)
}
+17 -21
View File
@@ -1,24 +1,20 @@
import React, { useCallback } from "react";
import { BlurView } from "expo-blur";
import { Pressable, Text } from "react-native";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { Entypo } from "@expo/vector-icons";
import { openShareSheet } from "../../utils/shareSheet";
import React, { useCallback } from 'react'
import { BlurView } from 'expo-blur'
import { Pressable, Text } from 'react-native'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { Entypo } from '@expo/vector-icons'
import { openShareSheet } from '../../utils/shareSheet'
export default function ShareBtn({
style,
onPress = null,
label = "Partager lexpérience",
}) {
export default function ShareBtn({ style, onPress = null, label = 'Partager lexpérience' }) {
const handlePress = useCallback(() => {
if (typeof onPress === "function") {
onPress();
return;
if (typeof onPress === 'function') {
onPress()
return
}
openShareSheet();
}, [onPress]);
openShareSheet()
}, [onPress])
return (
<Pressable
@@ -32,9 +28,9 @@ export default function ShareBtn({
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 15,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Text
@@ -50,5 +46,5 @@ export default function ShareBtn({
<Entypo name="share-alternative" size={16} color="white" />
</BlurView>
</Pressable>
);
)
}
+22 -23
View File
@@ -1,30 +1,29 @@
import { BlurView } from "expo-blur";
import { Image } from "expo-image";
import React from "react";
import { Platform, Pressable, Text } from "react-native";
import { icons } from "../assets";
import { Palette } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import alert from "./Alert";
import { BlurView } from 'expo-blur'
import { Image } from 'expo-image'
import React from 'react'
import { Platform, Pressable, Text } from 'react-native'
import { icons } from '../assets'
import { Palette } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
import alert from './Alert'
export default function ShareBtnWeb({ style }) {
const handleShare = () => {
const shareUrl =
typeof window !== "undefined" ? window.location.href : undefined;
if (typeof navigator !== "undefined" && navigator.share) {
const shareUrl = typeof window !== 'undefined' ? window.location.href : undefined
if (typeof navigator !== 'undefined' && navigator.share) {
navigator
.share({
title: "MusicLand",
text: "Découvre mon expérience sur MusicLand",
title: 'MusicLand',
text: 'Découvre mon expérience sur MusicLand',
...(shareUrl ? { url: shareUrl } : {}),
})
.catch(() => {
alert("Partage", "Le partage a été annulé.");
});
return;
alert('Partage', 'Le partage a été annulé.')
})
return
}
alert("Partage", "Fonctionnalité disponible prochainement.");
};
alert('Partage', 'Fonctionnalité disponible prochainement.')
}
return (
<Pressable
@@ -34,15 +33,15 @@ export default function ShareBtnWeb({ style }) {
onPress={handleShare}
>
<BlurView
intensity={Platform.OS === "web" ? 35 : 30}
intensity={Platform.OS === 'web' ? 35 : 30}
style={{
flexDirection: "row",
alignItems: "center",
flexDirection: 'row',
alignItems: 'center',
gap: 12,
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 999,
backgroundColor: "rgba(255, 255, 255, 0.1)",
backgroundColor: 'rgba(255, 255, 255, 0.1)',
}}
>
<Text
@@ -65,5 +64,5 @@ export default function ShareBtnWeb({ style }) {
/>
</BlurView>
</Pressable>
);
)
}
+24 -27
View File
@@ -1,21 +1,21 @@
import React, { useRef } from "react";
import { View, Text, Image, Animated, PanResponder } from "react-native";
import React, { useRef } from 'react'
import { View, Text, Image, Animated, PanResponder } from 'react-native'
import { Fonts, Palette, Style } from "../styles";
import { icons } from "../assets";
import { Fonts, Palette, Style } from '../styles'
import { icons } from '../assets'
const SlideToUnlock = ({ onUnlock, containerStyle = {} }) => {
const { width = 0 } = containerStyle;
const triggerPoint = width * 0.8; // Modification de la valeur du triggerPoint
const { width = 0 } = containerStyle
const triggerPoint = width * 0.8 // Modification de la valeur du triggerPoint
const slideAnim = useRef(new Animated.Value(0)).current;
const slideAnim = useRef(new Animated.Value(0)).current
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: (e, gestureState) => {
if (gestureState.dx > 0 && gestureState.dx < triggerPoint) {
slideAnim.setValue(gestureState.dx);
slideAnim.setValue(gestureState.dx)
}
},
onPanResponderRelease: (e, gestureState) => {
@@ -25,16 +25,16 @@ const SlideToUnlock = ({ onUnlock, containerStyle = {} }) => {
toValue: 0,
useNativeDriver: false,
onComplete: () => onUnlock(),
}).start();
}).start()
} else {
Animated.spring(slideAnim, {
toValue: 0,
useNativeDriver: false,
}).start();
}).start()
}
},
})
).current;
).current
return (
<View style={{ ...styles.container, ...containerStyle }}>
@@ -51,15 +51,12 @@ const SlideToUnlock = ({ onUnlock, containerStyle = {} }) => {
>
<Image
source={icons.arrowRight}
style={[
Style.iconDefault,
{ tintColor: Palette.darkRadioactivGreen },
]}
style={[Style.iconDefault, { tintColor: Palette.darkRadioactivGreen }]}
/>
</Animated.View>
</View>
);
};
)
}
const styles = {
container: {
@@ -67,14 +64,14 @@ const styles = {
backgroundColor: Palette.transparentRadioactivGreen,
borderRadius: 50,
borderWidth: 2,
borderStyle: "solid",
borderStyle: 'solid',
borderColor: Palette.radioactivGreen,
overflow: "hidden",
alignItems: "center",
justifyContent: "center",
overflow: 'hidden',
alignItems: 'center',
justifyContent: 'center',
},
slider: {
position: "absolute",
position: 'absolute',
left: 3,
top: 3,
width: 40,
@@ -83,14 +80,14 @@ const styles = {
borderRadius: 25,
},
text: {
position: "absolute",
position: 'absolute',
...Fonts({
color: "white",
color: 'white',
style: {
fontWeight: "bold",
fontWeight: 'bold',
},
}),
},
};
}
export default SlideToUnlock;
export default SlideToUnlock
+57 -64
View File
@@ -1,16 +1,12 @@
import { useEffect, useRef, useState } from "react";
import { StyleSheet, Text, View } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, {
runOnJS,
useAnimatedStyle,
useSharedValue,
} from "react-native-reanimated";
import { Palette, Style } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { LinearGradient } from "./LinearGradient/LinearGradient";
import { useEffect, useRef, useState } from 'react'
import { StyleSheet, Text, View } from 'react-native'
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import Animated, { runOnJS, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
import { Palette, Style } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
import { LinearGradient } from './LinearGradient/LinearGradient'
const INITIAL_BOX_SIZE = 6;
const INITIAL_BOX_SIZE = 6
export default ({
value,
@@ -21,38 +17,38 @@ export default ({
onSeekEnd,
seekEnabled = false,
}) => {
const offset = useSharedValue(0);
const boxWidth = useSharedValue(INITIAL_BOX_SIZE);
const [layout, setLayout] = useState(null);
const seekingRef = useRef(false);
const offset = useSharedValue(0)
const boxWidth = useSharedValue(INITIAL_BOX_SIZE)
const [layout, setLayout] = useState(null)
const seekingRef = useRef(false)
const SLIDER_WIDTH = layout?.width;
const MAX_VALUE = SLIDER_WIDTH - INITIAL_BOX_SIZE;
const SLIDER_WIDTH = layout?.width
const MAX_VALUE = SLIDER_WIDTH - INITIAL_BOX_SIZE
const handleSeekStart = () => {
if (seekEnabled && typeof onSeekStart === "function" && !seekingRef.current) {
seekingRef.current = true;
onSeekStart();
if (seekEnabled && typeof onSeekStart === 'function' && !seekingRef.current) {
seekingRef.current = true
onSeekStart()
}
};
}
const handleSeekEnd = () => {
if (seekingRef.current && typeof onSeekEnd === "function") {
seekingRef.current = false;
onSeekEnd();
if (seekingRef.current && typeof onSeekEnd === 'function') {
seekingRef.current = false
onSeekEnd()
}
};
}
const pan = Gesture.Pan()
.enabled(seekEnabled)
.onBegin(() => {
runOnJS(handleSeekStart)();
runOnJS(handleSeekStart)()
})
.onStart(() => {
runOnJS(handleSeekStart)();
runOnJS(handleSeekStart)()
})
.onChange((event) => {
runOnJS(handleSeekStart)();
runOnJS(handleSeekStart)()
offset.value =
Math.abs(offset.value) <= MAX_VALUE
? offset.value + event.changeX <= 0
@@ -60,58 +56,55 @@ export default ({
: offset.value + event.changeX >= MAX_VALUE
? MAX_VALUE
: offset.value + event.changeX
: offset.value;
: offset.value
const newWidth = INITIAL_BOX_SIZE + offset.value;
boxWidth.value = newWidth;
const newWidth = INITIAL_BOX_SIZE + offset.value
boxWidth.value = newWidth
})
.onEnd(() => {
if (seekEnabled && typeof onSeek === "function" && MAX_VALUE) {
const ratio =
MAX_VALUE > 0
? Math.min(1, Math.max(0, offset.value / MAX_VALUE))
: 0;
if (seekEnabled && typeof onSeek === 'function' && MAX_VALUE) {
const ratio = MAX_VALUE > 0 ? Math.min(1, Math.max(0, offset.value / MAX_VALUE)) : 0
// Reanimated -> JS thread bridge
runOnJS(onSeek)(ratio);
runOnJS(onSeek)(ratio)
}
runOnJS(handleSeekEnd)();
runOnJS(handleSeekEnd)()
})
.onFinalize(() => {
runOnJS(handleSeekEnd)();
});
runOnJS(handleSeekEnd)()
})
// Reflect external progress into the slider UI
useEffect(() => {
if (seekingRef.current) return;
if (seekingRef.current) return
if (typeof progress === "number" && layout?.width) {
const max = layout.width - INITIAL_BOX_SIZE;
const clamped = Math.max(0, Math.min(1, progress));
const newOffset = clamped * max;
offset.value = newOffset;
boxWidth.value = INITIAL_BOX_SIZE + newOffset;
if (typeof progress === 'number' && layout?.width) {
const max = layout.width - INITIAL_BOX_SIZE
const clamped = Math.max(0, Math.min(1, progress))
const newOffset = clamped * max
offset.value = newOffset
boxWidth.value = INITIAL_BOX_SIZE + newOffset
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [progress, layout?.width]);
}, [progress, layout?.width])
const boxStyle = useAnimatedStyle(() => {
return {
width: INITIAL_BOX_SIZE + offset.value,
};
});
}
})
const sliderStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: offset.value }],
};
});
}
})
return (
<View onLayout={(e) => setLayout(e.nativeEvent.layout)}>
<View style={{ ...styles.sliderTrack, width: SLIDER_WIDTH }}>
<Animated.View style={[styles.box, boxStyle]}>
<LinearGradient
colors={["#F94697", "#7023F7"]}
colors={['#F94697', '#7023F7']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={{ flex: 1, borderRadius: 20 }}
@@ -126,26 +119,26 @@ export default ({
<Text style={styles.time}>{maxValue}</Text>
</View>
</View>
);
};
)
}
const styles = StyleSheet.create({
box: {
height: INITIAL_BOX_SIZE,
borderRadius: 20,
position: "absolute",
position: 'absolute',
zIndex: 1,
},
sliderHandle: {
width: 20,
height: 20,
backgroundColor: "#f8f9ff",
backgroundColor: '#f8f9ff',
borderRadius: 25,
position: "absolute",
position: 'absolute',
zIndex: 2,
borderWidth: 4,
borderColor: "#9B4DFF",
shadowColor: "#8951FC",
borderColor: '#9B4DFF',
shadowColor: '#8951FC',
shadowOffset: {
width: 0,
height: 3,
@@ -156,13 +149,13 @@ const styles = StyleSheet.create({
},
sliderTrack: {
height: 6,
backgroundColor: "#0F0C19",
backgroundColor: '#0F0C19',
borderRadius: 25,
justifyContent: "center",
justifyContent: 'center',
},
time: {
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
},
});
})
+64 -69
View File
@@ -1,13 +1,13 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { StyleSheet, Text, View } from "react-native";
import { Palette, Style } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { LinearGradient } from "./LinearGradient/LinearGradient";
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { StyleSheet, Text, View } from 'react-native'
import { Palette, Style } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
import { LinearGradient } from './LinearGradient/LinearGradient'
const INITIAL_BOX_SIZE = 6;
const HANDLE_SIZE = 20;
const INITIAL_BOX_SIZE = 6
const HANDLE_SIZE = 20
const clamp01 = (value) => Math.min(1, Math.max(0, value));
const clamp01 = (value) => Math.min(1, Math.max(0, value))
const Slider = ({
value,
@@ -18,85 +18,80 @@ const Slider = ({
onSeekEnd,
seekEnabled = false,
}) => {
const [layoutWidth, setLayoutWidth] = useState(0);
const [ratio, setRatio] = useState(
typeof progress === "number" ? clamp01(progress) : 0
);
const draggingRef = useRef(false);
const [layoutWidth, setLayoutWidth] = useState(0)
const [ratio, setRatio] = useState(typeof progress === 'number' ? clamp01(progress) : 0)
const draggingRef = useRef(false)
useEffect(() => {
if (!draggingRef.current && typeof progress === "number") {
setRatio(clamp01(progress));
if (!draggingRef.current && typeof progress === 'number') {
setRatio(clamp01(progress))
}
}, [progress]);
}, [progress])
const updateRatioFromX = useCallback(
(x) => {
if (layoutWidth <= 0) return;
const available = Math.max(layoutWidth - INITIAL_BOX_SIZE, 0);
if (layoutWidth <= 0) return
const available = Math.max(layoutWidth - INITIAL_BOX_SIZE, 0)
if (available <= 0) {
setRatio(0);
return;
setRatio(0)
return
}
const clampedX = Math.min(Math.max(x, 0), layoutWidth);
const nextRatio = clamp01(clampedX / available);
setRatio(nextRatio);
const clampedX = Math.min(Math.max(x, 0), layoutWidth)
const nextRatio = clamp01(clampedX / available)
setRatio(nextRatio)
},
[layoutWidth]
);
)
const handleGrant = useCallback(
(event) => {
if (!seekEnabled) return;
draggingRef.current = true;
updateRatioFromX(event?.nativeEvent?.locationX || 0);
if (typeof onSeekStart === "function") {
onSeekStart();
if (!seekEnabled) return
draggingRef.current = true
updateRatioFromX(event?.nativeEvent?.locationX || 0)
if (typeof onSeekStart === 'function') {
onSeekStart()
}
},
[seekEnabled, updateRatioFromX, onSeekStart]
);
)
const handleMove = useCallback(
(event) => {
if (!seekEnabled || !draggingRef.current) return;
updateRatioFromX(event?.nativeEvent?.locationX || 0);
if (!seekEnabled || !draggingRef.current) return
updateRatioFromX(event?.nativeEvent?.locationX || 0)
},
[seekEnabled, updateRatioFromX]
);
)
const finishSeeking = useCallback(() => {
if (!seekEnabled || !draggingRef.current) return;
draggingRef.current = false;
const currentRatio = clamp01(ratio);
if (typeof onSeek === "function") {
onSeek(currentRatio);
if (!seekEnabled || !draggingRef.current) return
draggingRef.current = false
const currentRatio = clamp01(ratio)
if (typeof onSeek === 'function') {
onSeek(currentRatio)
}
if (typeof onSeekEnd === "function") {
onSeekEnd();
if (typeof onSeekEnd === 'function') {
onSeekEnd()
}
}, [seekEnabled, ratio, onSeek, onSeekEnd]);
}, [seekEnabled, ratio, onSeek, onSeekEnd])
const handleLayout = useCallback(
(event) => {
const width = event?.nativeEvent?.layout?.width || 0;
const width = event?.nativeEvent?.layout?.width || 0
if (width !== layoutWidth) {
setLayoutWidth(width);
setLayoutWidth(width)
}
},
[layoutWidth]
);
)
const maxOffset = Math.max(layoutWidth - INITIAL_BOX_SIZE, 0);
const offset = maxOffset * ratio;
const maxOffset = Math.max(layoutWidth - INITIAL_BOX_SIZE, 0)
const offset = maxOffset * ratio
return (
<View style={styles.container}>
<View
style={[
styles.interactionArea,
seekEnabled ? styles.pointerEnabled : null,
]}
style={[styles.interactionArea, seekEnabled ? styles.pointerEnabled : null]}
onLayout={handleLayout}
onStartShouldSetResponderCapture={() => seekEnabled}
onStartShouldSetResponder={() => seekEnabled}
@@ -109,7 +104,7 @@ const Slider = ({
<View style={styles.sliderTrack}>
<View style={[styles.box, { width: INITIAL_BOX_SIZE + offset }]}>
<LinearGradient
colors={["#F94697", "#7023F7"]}
colors={['#F94697', '#7023F7']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={{ flex: 1, borderRadius: 20 }}
@@ -123,49 +118,49 @@ const Slider = ({
<Text style={styles.time}>{maxValue}</Text>
</View>
</View>
);
};
)
}
const styles = StyleSheet.create({
container: {
width: "100%",
width: '100%',
},
interactionArea: {
width: "100%",
width: '100%',
height: HANDLE_SIZE,
justifyContent: "center",
position: "relative",
justifyContent: 'center',
position: 'relative',
},
pointerEnabled: {
cursor: "pointer",
cursor: 'pointer',
},
sliderTrack: {
width: "100%",
width: '100%',
height: INITIAL_BOX_SIZE,
backgroundColor: "#0F0C19",
backgroundColor: '#0F0C19',
borderRadius: 25,
overflow: "hidden",
overflow: 'hidden',
},
box: {
height: INITIAL_BOX_SIZE,
borderRadius: 20,
position: "absolute",
position: 'absolute',
left: 0,
top: 0,
zIndex: 1,
overflow: "hidden",
overflow: 'hidden',
},
sliderHandle: {
width: HANDLE_SIZE,
height: HANDLE_SIZE,
backgroundColor: "#f8f9ff",
backgroundColor: '#f8f9ff',
borderRadius: HANDLE_SIZE / 2,
position: "absolute",
position: 'absolute',
// top: HANDLE_TOP_OFFSET,
zIndex: 2,
borderWidth: 4,
borderColor: "#9B4DFF",
shadowColor: "#8951FC",
borderColor: '#9B4DFF',
shadowColor: '#8951FC',
shadowOffset: {
width: 0,
height: 3,
@@ -178,6 +173,6 @@ const styles = StyleSheet.create({
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
},
});
})
export default Slider;
export default Slider
+80 -85
View File
@@ -1,32 +1,32 @@
import {
View,
Text,
ScrollView,
StyleSheet,
Dimensions,
TouchableOpacity,
} from "react-native";
import React, { useEffect, useMemo, useState } from "react";
import { View, Text, ScrollView, StyleSheet, Dimensions, TouchableOpacity } from 'react-native'
import React, { useEffect, useMemo, useState } from 'react'
import Animated, {
runOnJS,
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withSpring,
} from "react-native-reanimated";
import { PanGestureHandler } from "react-native-gesture-handler";
} from 'react-native-reanimated'
import { PanGestureHandler } from 'react-native-gesture-handler'
const { width } = Dimensions.get("window");
const { width } = Dimensions.get('window')
// Child component to respect Rules of Hooks (no hooks in loops)
const DraggableItem = ({ item, gestureHandler, draggingItem, translateX, translateY, onActivate }) => {
const DraggableItem = ({
item,
gestureHandler,
draggingItem,
translateX,
translateY,
onActivate,
}) => {
const style = useAnimatedStyle(() => ({
transform: [
{ translateX: draggingItem.value === item.id ? translateX.value : 0 },
{ translateY: draggingItem.value === item.id ? translateY.value : 0 },
],
zIndex: draggingItem.value === item.id ? 10 : 0,
}));
}))
return (
<PanGestureHandler
@@ -37,8 +37,8 @@ const DraggableItem = ({ item, gestureHandler, draggingItem, translateX, transla
<Text style={styles.text}>{item.label ?? String(item)}</Text>
</Animated.View>
</PanGestureHandler>
);
};
)
}
// Props:
// - sourceItems: array of { id, label, value } or strings
@@ -47,88 +47,83 @@ const DraggableItem = ({ item, gestureHandler, draggingItem, translateX, transla
const SongStructureDragDrop = ({ sourceItems, initialSelected, onChange }) => {
const normalize = (arr = []) =>
arr.map((item, idx) =>
typeof item === "string"
? { id: `${item}-${idx}`, label: item, value: item }
: item,
);
typeof item === 'string' ? { id: `${item}-${idx}`, label: item, value: item } : item
)
const normalizedSource = useMemo(
() => normalize(sourceItems || []),
[sourceItems],
);
const normalizedSource = useMemo(() => normalize(sourceItems || []), [sourceItems])
const [rightItems, setRightItems] = useState([]);
const [rightItems, setRightItems] = useState([])
// Compare arrays by item id and order to avoid unnecessary state churn
const sameById = (a = [], b = []) => {
if (a === b) return true;
if (!a || !b) return false;
if (a.length !== b.length) return false;
if (a === b) return true
if (!a || !b) return false
if (a.length !== b.length) return false
for (let idx = 0; idx < a.length; idx++) {
if (a[idx]?.id !== b[idx]?.id) return false;
if (a[idx]?.id !== b[idx]?.id) return false
}
return true;
};
return true
}
useEffect(() => {
const selected = Array.isArray(initialSelected) ? initialSelected : [];
const selectedSet = new Set(selected);
const nextRight = normalizedSource.filter((i) => selectedSet.has(i.id));
setRightItems((prev) => (sameById(prev, nextRight) ? prev : nextRight));
}, [normalizedSource, initialSelected]);
const selected = Array.isArray(initialSelected) ? initialSelected : []
const selectedSet = new Set(selected)
const nextRight = normalizedSource.filter((i) => selectedSet.has(i.id))
setRightItems((prev) => (sameById(prev, nextRight) ? prev : nextRight))
}, [normalizedSource, initialSelected])
const leftItems = useMemo(() => {
if (!rightItems || rightItems.length === 0) return normalizedSource;
const rightIds = new Set(rightItems.map((item) => item.id));
return normalizedSource.filter((item) => !rightIds.has(item.id));
}, [normalizedSource, rightItems]);
if (!rightItems || rightItems.length === 0) return normalizedSource
const rightIds = new Set(rightItems.map((item) => item.id))
return normalizedSource.filter((item) => !rightIds.has(item.id))
}, [normalizedSource, rightItems])
const draggingItem = useSharedValue(null);
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const draggingItem = useSharedValue(null)
const translateX = useSharedValue(0)
const translateY = useSharedValue(0)
const onActivate = (id) => {
draggingItem.value = id;
};
draggingItem.value = id
}
const moveItem = (itemId) => {
const item = normalizedSource.find((i) => i.id === itemId);
if (!item) return;
const item = normalizedSource.find((i) => i.id === itemId)
if (!item) return
setRightItems((current) => {
if (current.some((i) => i.id === itemId)) return current;
const next = [...current, item];
onChange?.(next.map((i) => i.value ?? i.label));
return next;
});
};
if (current.some((i) => i.id === itemId)) return current
const next = [...current, item]
onChange?.(next.map((i) => i.value ?? i.label))
return next
})
}
const removeItem = (itemId) => {
setRightItems((current) => {
if (!current.some((i) => i.id === itemId)) return current;
const next = current.filter((i) => i.id !== itemId);
onChange?.(next.map((i) => i.value ?? i.label));
return next;
});
};
if (!current.some((i) => i.id === itemId)) return current
const next = current.filter((i) => i.id !== itemId)
onChange?.(next.map((i) => i.value ?? i.label))
return next
})
}
const gestureHandler = useAnimatedGestureHandler({
onStart: (_, ctx) => {
ctx.startX = translateX.value;
ctx.startY = translateY.value;
ctx.startX = translateX.value
ctx.startY = translateY.value
},
onActive: (event, ctx) => {
translateX.value = ctx.startX + event.translationX;
translateY.value = ctx.startY + event.translationY;
translateX.value = ctx.startX + event.translationX
translateY.value = ctx.startY + event.translationY
},
onEnd: () => {
if (translateX.value > width / 2) {
runOnJS(moveItem)(draggingItem.value);
runOnJS(moveItem)(draggingItem.value)
}
translateX.value = withSpring(0);
translateY.value = withSpring(0);
draggingItem.value = null;
translateX.value = withSpring(0)
translateY.value = withSpring(0)
draggingItem.value = null
},
});
})
return (
<View style={styles.scrollWrapper}>
@@ -165,55 +160,55 @@ const SongStructureDragDrop = ({ sourceItems, initialSelected, onChange }) => {
))}
</ScrollView>
</View>
);
};
)
}
export default SongStructureDragDrop;
export default SongStructureDragDrop
const styles = StyleSheet.create({
scrollWrapper: {
flexDirection: "row",
justifyContent: "space-around",
flexDirection: 'row',
justifyContent: 'space-around',
},
scroll: {
width: width / 2.2,
height: "90%",
backgroundColor: "#f0f0f0",
height: '90%',
backgroundColor: '#f0f0f0',
borderRadius: 10,
margin: 5,
padding: 10,
},
title: {
fontWeight: "bold",
fontWeight: 'bold',
fontSize: 16,
marginBottom: 10,
},
item: {
backgroundColor: "#aaf",
backgroundColor: '#aaf',
padding: 15,
marginVertical: 5,
borderRadius: 10,
},
selectedItem: {
backgroundColor: "#bdf",
backgroundColor: '#bdf',
padding: 15,
marginVertical: 5,
borderRadius: 10,
alignItems: "center",
alignItems: 'center',
},
helpText: {
fontSize: 12,
color: "#666",
color: '#666',
marginBottom: 8,
textAlign: "center",
textAlign: 'center',
},
removeHint: {
fontSize: 12,
color: "#555",
color: '#555',
marginTop: 4,
},
text: {
color: "#333",
textAlign: "center",
color: '#333',
textAlign: 'center',
},
});
})
+16 -20
View File
@@ -1,32 +1,28 @@
import React from "react";
import { Image } from "react-native";
import { subBadges } from "../assets";
import React from 'react'
import { Image } from 'react-native'
import { subBadges } from '../assets'
const allowedLevels = new Set(["starter", "pro", "premium"]);
const allowedLevels = new Set(['starter', 'pro', 'premium'])
const normalizeLevel = (value) => {
if (typeof value !== "string") {
return null;
if (typeof value !== 'string') {
return null
}
const normalized = value.trim().toLowerCase();
return allowedLevels.has(normalized) ? normalized : null;
};
const normalized = value.trim().toLowerCase()
return allowedLevels.has(normalized) ? normalized : null
}
const SubscriptionBadge = ({ level = null, size = 20, style = null }) => {
const normalized = normalizeLevel(level);
const source = normalized ? subBadges[normalized] : null;
const normalized = normalizeLevel(level)
const source = normalized ? subBadges[normalized] : null
if (!source) {
return null;
return null
}
return (
<Image
source={source}
style={[{ width: size, height: size }, style]}
resizeMode="contain"
/>
);
};
<Image source={source} style={[{ width: size, height: size }, style]} resizeMode="contain" />
)
}
export default React.memo(SubscriptionBadge);
export default React.memo(SubscriptionBadge)
+32 -39
View File
@@ -1,49 +1,42 @@
import React from "react";
import { StyleSheet, Text, View } from "react-native";
import { gutters, Palette } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import BorderGradientButton from "./BorderGradientButton";
import GradientButton from "./GradientButton";
import Overlay from "./Overlay";
import React from 'react'
import { StyleSheet, Text, View } from 'react-native'
import { gutters, Palette } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
import BorderGradientButton from './BorderGradientButton'
import GradientButton from './GradientButton'
import Overlay from './Overlay'
const SubscriptionConfirmModal = ({
isVisible,
setIsVisible,
onJoinClub,
onContinue,
continueLabel = "Continuer vers la création de playback",
continueLabel = 'Continuer vers la création de playback',
}) => {
const handleJoinClub = () => {
if (typeof setIsVisible === "function") {
setIsVisible(false);
if (typeof setIsVisible === 'function') {
setIsVisible(false)
}
if (typeof onJoinClub === "function") {
onJoinClub();
if (typeof onJoinClub === 'function') {
onJoinClub()
}
};
}
const handleContinue = () => {
if (typeof setIsVisible === "function") {
setIsVisible(false);
if (typeof setIsVisible === 'function') {
setIsVisible(false)
}
if (typeof onContinue === "function") {
onContinue();
if (typeof onContinue === 'function') {
onContinue()
}
};
}
return (
<Overlay
isVisible={isVisible}
setIsVisible={setIsVisible}
blurIntensity={15}
>
<Overlay isVisible={isVisible} setIsVisible={setIsVisible} blurIntensity={15}>
<View style={styles.modalCard}>
<Text style={styles.modalTitle}>
Continuer sans générer de revenus ?
</Text>
<Text style={styles.modalTitle}>Continuer sans générer de revenus ?</Text>
<Text style={styles.modalDescription}>
Rejoins le Club Musicland pour monétiser tes écoutes et accéder aux
concours.
Rejoins le Club Musicland pour monétiser tes écoutes et accéder aux concours.
</Text>
<View style={styles.modalActions}>
<GradientButton
@@ -59,41 +52,41 @@ const SubscriptionConfirmModal = ({
</View>
</View>
</Overlay>
);
};
)
}
export default SubscriptionConfirmModal;
export default SubscriptionConfirmModal
const styles = StyleSheet.create({
modalCard: {
width: "90%",
width: '90%',
maxWidth: 420,
alignSelf: "center",
alignSelf: 'center',
padding: gutters * 1.4,
borderRadius: 20,
backgroundColor: "rgba(37, 36, 56, 0.96)",
backgroundColor: 'rgba(37, 36, 56, 0.96)',
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.12)",
borderColor: 'rgba(255, 255, 255, 0.12)',
gap: gutters * 0.8,
},
modalTitle: {
fontFamily: FONT_FAMILY.InterSemiBold,
fontSize: 18,
color: Palette.white,
textAlign: "center",
textAlign: 'center',
},
modalDescription: {
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 14,
lineHeight: 20,
color: Palette.grayMid,
textAlign: "center",
textAlign: 'center',
},
modalActions: {
gap: gutters * 0.6,
marginTop: gutters * 0.6,
},
modalAction: {
width: "100%",
width: '100%',
},
});
})
+15 -15
View File
@@ -1,28 +1,28 @@
import { Motion } from "@legendapp/motion";
import { Pressable, View } from "react-native";
import { Easing } from "react-native-reanimated";
import { Motion } from '@legendapp/motion'
import { Pressable, View } from 'react-native'
import { Easing } from 'react-native-reanimated'
import { Palette } from "../styles";
import { LinearGradient } from "./LinearGradient/LinearGradient";
import { Palette } from '../styles'
import { LinearGradient } from './LinearGradient/LinearGradient'
export default ({ value, setValue, containerStyle = {} }) => {
const currentColor = value ? ["#F94697", "#7023F7"] : ["#8C8C8C", "#8C8C8C"];
const currentColor = value ? ['#F94697', '#7023F7'] : ['#8C8C8C', '#8C8C8C']
const switchHeight = 28;
const internalMargin = 2;
const switchHeight = 28
const internalMargin = 2
const itemSize = switchHeight - internalMargin * 2;
const itemSize = switchHeight - internalMargin * 2
return (
<Pressable onPress={() => setValue(!value)}>
<LinearGradient
colors={currentColor}
style={{
flexDirection: "row",
flexDirection: 'row',
width: switchHeight * 2,
height: switchHeight,
borderRadius: 25,
backgroundColor: "red",
backgroundColor: 'red',
...containerStyle,
}}
>
@@ -32,12 +32,12 @@ export default ({ value, setValue, containerStyle = {} }) => {
left: value ? switchHeight : 0,
}}
transition={{
type: "timing",
type: 'timing',
duration: 300,
easing: Easing.easing,
}}
style={{
position: "absolute",
position: 'absolute',
width: itemSize,
height: itemSize,
margin: internalMargin,
@@ -47,5 +47,5 @@ export default ({ value, setValue, containerStyle = {} }) => {
/>
</LinearGradient>
</Pressable>
);
};
)
}
+35 -40
View File
@@ -1,8 +1,8 @@
import { View, Text, ScrollView, Pressable } from "react-native";
import { View, Text, ScrollView, Pressable } from 'react-native'
import { Style, Fonts, gutters, Palette } from "../styles";
import { Input } from "./Input";
import { mainBorderRadius } from "../styles/Style";
import { Style, Fonts, gutters, Palette } from '../styles'
import { Input } from './Input'
import { mainBorderRadius } from '../styles/Style'
const Tableau = ({
headings = [],
@@ -16,7 +16,7 @@ const Tableau = ({
mainColor = Palette.primary,
transparentColor = Palette.transparentPrimary,
}) => {
const filteredHeadings = headings.filter(({ condition = true }) => condition);
const filteredHeadings = headings.filter(({ condition = true }) => condition)
return (
<>
@@ -24,26 +24,25 @@ const Tableau = ({
style={{
...Style.containerSpaceBetween,
...Style.containerItem,
backgroundColor: "transparent",
backgroundColor: 'transparent',
padding: gutters / 2,
paddingVertical: gutters / 4,
}}
>
{filteredHeadings.map(({ title = "", flex }, index) => (
{filteredHeadings.map(({ title = '', flex }, index) => (
<View style={{ flex, paddingRight: 10 }}>
<Text
key={index}
style={Fonts({
type: "default",
type: 'default',
style: {
textAlign: !index
? "left"
? 'left'
: index === filteredHeadings?.length - 1
? "right"
: "center",
? 'right'
: 'center',
color: mainColor,
borderRightWidth:
index !== filteredHeadings?.length - 1 ? 1 : 0,
borderRightWidth: index !== filteredHeadings?.length - 1 ? 1 : 0,
borderRightColor: transparentColor,
},
})}
@@ -71,33 +70,29 @@ const Tableau = ({
onPress={() => onPressRow?.({ item })}
style={{
...Style.containerSpaceBetween,
alignItems: "flex-start",
alignItems: 'flex-start',
paddingHorizontal: gutters / 2,
}}
>
{filteredHeadings.map(
(
{ key, type = "DEFAULT", textStyle = () => {} },
indexColumn
) => {
const { flex, isEditable, renderCell } =
filteredHeadings[indexColumn];
({ key, type = 'DEFAULT', textStyle = () => {} }, indexColumn) => {
const { flex, isEditable, renderCell } = filteredHeadings[indexColumn]
const content = renderCell({
item,
key,
indexItemList,
});
})
return (
<View style={{ flex, paddingRight: 10 }}>
{type === "CUSTOM" ? (
{type === 'CUSTOM' ? (
content
) : isEdit && isEditable ? (
<Input
key={indexColumn}
placeholder={""}
value={item?.[key] || ""}
placeholder={''}
value={item?.[key] || ''}
setValue={(value) =>
onUpdate({
index: indexItemList,
@@ -105,28 +100,28 @@ const Tableau = ({
value,
})
}
type={!indexColumn ? "textarea" : "default"}
type={!indexColumn ? 'textarea' : 'default'}
borderType="dashed"
containerStyle={{
borderRadius: mainBorderRadius / 2,
width: "100%",
backgroundColor: "transparent", // 'white
width: '100%',
backgroundColor: 'transparent', // 'white
}}
textInputStyle={{
textAlign: !indexColumn
? "left"
? 'left'
: indexColumn === filteredHeadings?.length - 1
? "right"
: "center",
? 'right'
: 'center',
}}
/>
) : (
<Text
key={indexColumn}
style={Fonts({
type: "default",
type: 'default',
style: {
textAlign: !indexColumn ? "left" : "center",
textAlign: !indexColumn ? 'left' : 'center',
...textStyle({ item }),
},
})}
@@ -135,21 +130,21 @@ const Tableau = ({
</Text>
)}
</View>
);
)
}
)}
</Pressable>
<View
style={{
width: "100%",
width: '100%',
backgroundColor: transparentColor,
height: 1,
marginVertical: gutters / 2,
}}
/>
</>
);
)
})}
</ScrollView>
) : (
@@ -161,9 +156,9 @@ const Tableau = ({
>
<Text
style={Fonts({
type: "default",
type: 'default',
style: {
textAlign: "center",
textAlign: 'center',
color: mainColor,
},
})}
@@ -173,7 +168,7 @@ const Tableau = ({
</View>
)}
</>
);
};
)
}
export default Tableau;
export default Tableau
+10 -15
View File
@@ -1,23 +1,18 @@
import { View } from "react-native";
import { useState } from "react";
import { Motion } from "@legendapp/motion";
import { View } from 'react-native'
import { useState } from 'react'
import { Motion } from '@legendapp/motion'
import { Palette, Style } from "../styles";
import { Palette, Style } from '../styles'
export default () => {
const [animatedDotIndex, setAnimatedDotIndex] = useState(0);
const [animatedDotIndex, setAnimatedDotIndex] = useState(0)
setTimeout(() => {
setAnimatedDotIndex((animatedDotIndex + 1) % 3);
}, 500);
setAnimatedDotIndex((animatedDotIndex + 1) % 3)
}, 500)
return (
<View
style={[
Style.containerSpaceBetween,
{ width: 60, padding: 10, paddingVertical: 5 },
]}
>
<View style={[Style.containerSpaceBetween, { width: 60, padding: 10, paddingVertical: 5 }]}>
{[1, 2, 3].map((item) => (
<Motion.View
key={item}
@@ -33,5 +28,5 @@ export default () => {
/>
))}
</View>
);
};
)
}
+2 -2
View File
@@ -1,3 +1,3 @@
import { WebView as NativeWebView } from "react-native-webview";
import { WebView as NativeWebView } from 'react-native-webview'
export const WebView = NativeWebView;
export const WebView = NativeWebView
+17 -17
View File
@@ -1,39 +1,39 @@
import React, { useEffect, useRef } from "react";
import React, { useEffect, useRef } from 'react'
export const WebView = ({ source, allowFullScreen, onURLChange }) => {
const iframeRef = useRef(null);
const iframeRef = useRef(null)
useEffect(() => {
// Définition de la fonction handleLoad à l'intérieur du useEffect
const handleLoad = () => {
try {
const currentUrl = iframeRef.current?.contentWindow?.location?.href;
onURLChange(currentUrl); // Appeler la fonction de callback avec l'URL actuelle
const currentUrl = iframeRef.current?.contentWindow?.location?.href
onURLChange(currentUrl) // Appeler la fonction de callback avec l'URL actuelle
} catch (error) {
console.error("Erreur d'accès à l'URL de l'iframe:", error);
console.error("Erreur d'accès à l'URL de l'iframe:", error)
}
};
}
const iframeElement = iframeRef.current;
iframeElement.addEventListener("load", handleLoad);
const iframeElement = iframeRef.current
iframeElement.addEventListener('load', handleLoad)
return () => {
iframeElement.removeEventListener("load", handleLoad);
};
}, [onURLChange]); // Assurez-vous que cette dépendance est correctement définie pour éviter des appels excessifs
iframeElement.removeEventListener('load', handleLoad)
}
}, [onURLChange]) // Assurez-vous que cette dépendance est correctement définie pour éviter des appels excessifs
return (
<iframe
ref={iframeRef}
src={source?.uri}
style={{
display: "flex",
border: "none",
width: "100%",
height: "100vh",
display: 'flex',
border: 'none',
width: '100%',
height: '100vh',
}}
title="minuit.starter"
allowFullScreen={allowFullScreen}
/>
);
};
)
}
+123 -138
View File
@@ -4,122 +4,106 @@ import {
BottomSheetModal,
BottomSheetScrollView,
BottomSheetTextInput,
} from '@gorhom/bottom-sheet';
import { BlurView } from 'expo-blur';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
Image,
Platform,
Pressable,
Text,
View,
KeyboardAvoidingView,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { icons } from '../../assets';
import { increment, projectsRef, serverTimestamp } from '../../config/firebase';
import useDataFromRef from '../../hooks/useDataFromRef';
import { useUser } from '../../providers/UserDataProvider';
import { getArtistDisplayName } from '../../utils/artistName';
import { Palette } from '../../styles';
import { FONT_FAMILY } from '../../styles/Fonts';
import ProfilePicture from '../ProfilePicture';
import { ensureAuthenticated } from '../../utils/authRedirect';
} from '@gorhom/bottom-sheet'
import { BlurView } from 'expo-blur'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Image, Platform, Pressable, Text, View, KeyboardAvoidingView } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { icons } from '../../assets'
import { increment, projectsRef, serverTimestamp } from '../../config/firebase'
import useDataFromRef from '../../hooks/useDataFromRef'
import { useUser } from '../../providers/UserDataProvider'
import { getArtistDisplayName } from '../../utils/artistName'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import ProfilePicture from '../ProfilePicture'
import { ensureAuthenticated } from '../../utils/authRedirect'
let externalOpen;
let externalClose;
let externalOpen
let externalClose
export const openComments = (projectId) => {
if (typeof externalOpen === 'function') externalOpen(projectId);
};
if (typeof externalOpen === 'function') externalOpen(projectId)
}
export const closeComments = () => {
if (typeof externalClose === 'function') externalClose();
};
if (typeof externalClose === 'function') externalClose()
}
const formatRelativeTime = (date) => {
try {
const d = date instanceof Date ? date : date?.toDate?.() || null;
if (!d) return '';
const diff = Math.max(0, Date.now() - d.getTime());
const sec = Math.floor(diff / 1000);
if (sec < 60) return `${sec}s`;
const min = Math.floor(sec / 60);
if (min < 60) return `${min}min`;
const h = Math.floor(min / 60);
if (h < 24) return `${h}h`;
const dA = Math.floor(h / 24);
return `${dA}j`;
const d = date instanceof Date ? date : date?.toDate?.() || null
if (!d) return ''
const diff = Math.max(0, Date.now() - d.getTime())
const sec = Math.floor(diff / 1000)
if (sec < 60) return `${sec}s`
const min = Math.floor(sec / 60)
if (min < 60) return `${min}min`
const h = Math.floor(min / 60)
if (h < 24) return `${h}h`
const dA = Math.floor(h / 24)
return `${dA}j`
} catch (e) {
return '';
return ''
}
};
}
const CommentsBottomSheet = () => {
const modalRef = useRef(null);
const insets = useSafeAreaInsets();
const { currentUID, currentUserData } = useUser();
const modalRef = useRef(null)
const insets = useSafeAreaInsets()
const { currentUID, currentUserData } = useUser()
const currentUserDisplayName = useMemo(
() => getArtistDisplayName(currentUserData, ''),
[currentUserData]
);
const scrollRef = useRef(null);
)
const scrollRef = useRef(null)
const [projectId, setProjectId] = useState(null);
const [text, setText] = useState('');
const [typing, setTyping] = useState(false);
const [projectId, setProjectId] = useState(null)
const [text, setText] = useState('')
const [typing, setTyping] = useState(false)
const requireAuth = useCallback(() => {
return ensureAuthenticated(currentUID, {
onIntercept: () => {
try {
modalRef.current?.dismiss?.();
modalRef.current?.dismiss?.()
} catch (_error) {}
},
});
}, [currentUID]);
})
}, [currentUID])
useEffect(() => {
externalOpen = (pid) => {
setProjectId(pid || null);
requestAnimationFrame(() => modalRef.current?.present());
};
setProjectId(pid || null)
requestAnimationFrame(() => modalRef.current?.present())
}
externalClose = () => {
try {
modalRef.current?.dismiss?.();
modalRef.current?.dismiss?.()
} catch {}
};
}
return () => {
externalOpen = undefined;
externalClose = undefined;
};
}, []);
externalOpen = undefined
externalClose = undefined
}
}, [])
const snapPoints = useMemo(() => ['90%'], []);
const snapPoints = useMemo(() => ['90%'], [])
const INPUT_HEIGHT = 54;
const INPUT_MARGIN_VERTICAL = 12;
const INPUT_HEIGHT = 54
const INPUT_MARGIN_VERTICAL = 12
const bottomPadding = useMemo(
() => INPUT_HEIGHT + INPUT_MARGIN_VERTICAL + (insets?.bottom || 0) + 12,
[insets?.bottom]
);
)
const commentsRef = useMemo(() => {
try {
return projectId
? projectsRef
.doc(projectId)
.collection('comments')
.orderBy('createdAt', 'desc')
: null;
? projectsRef.doc(projectId).collection('comments').orderBy('createdAt', 'desc')
: null
} catch {
return null;
return null
}
}, [projectId]);
}, [projectId])
const {
data: comments = [],
@@ -134,14 +118,14 @@ const CommentsBottomSheet = () => {
refreshArray: [projectId],
usePagination: true,
batchSize: 20,
});
})
const onSend = async () => {
const value = (text || '').trim();
if (!value || !projectId) return;
if (!requireAuth()) return;
const value = (text || '').trim()
if (!value || !projectId) return
if (!requireAuth()) return
try {
setText('');
setText('')
const docRef = await projectsRef
.doc(projectId)
.collection('comments')
@@ -151,11 +135,9 @@ const CommentsBottomSheet = () => {
profilePicture: currentUserData?.profilePictureURL || '',
text: value,
createdAt: serverTimestamp(),
});
})
try {
await projectsRef
.doc(projectId)
.set({ commentsCount: increment(1) }, { merge: true });
await projectsRef.doc(projectId).set({ commentsCount: increment(1) }, { merge: true })
} catch {}
// Optimistic: ajouter immédiatement le commentaire en tête de liste
const optimistic = {
@@ -165,15 +147,12 @@ const CommentsBottomSheet = () => {
profilePicture: currentUserData?.profilePictureURL || '',
text: value,
createdAt: new Date(),
};
setComments((prev = []) => [
optimistic,
...prev.filter((x) => x?.id !== optimistic.id),
]);
}
setComments((prev = []) => [optimistic, ...prev.filter((x) => x?.id !== optimistic.id)])
} catch {
// ignore
}
};
}
const renderBackdrop = useCallback(
(props) => (
@@ -181,48 +160,48 @@ const CommentsBottomSheet = () => {
{...props}
appearsOnIndex={0}
disappearsOnIndex={-1}
pressBehavior='close'
pressBehavior="close"
/>
),
[]
);
)
const handleScroll = useCallback(
({ nativeEvent }) => {
try {
const { layoutMeasurement, contentOffset, contentSize } =
nativeEvent || {};
const paddingToBottom = 80;
const { layoutMeasurement, contentOffset, contentSize } = nativeEvent || {}
const paddingToBottom = 80
if (
layoutMeasurement?.height + contentOffset?.y >=
(contentSize?.height || 0) - paddingToBottom
) {
loadMore?.();
loadMore?.()
}
} catch {}
},
[loadMore]
);
)
return (
<BottomSheetModal
ref={modalRef}
snapPoints={snapPoints}
keyboardBehavior='interactive' // linput est dans le contenu → mode interactif OK
keyboardBlurBehavior='restore'
keyboardBehavior="interactive" // linput est dans le contenu → mode interactif OK
keyboardBlurBehavior="restore"
enablePanDownToClose
enableContentPanningGesture
stackBehavior='push'
stackBehavior="push"
topInset={insets.top}
bottomInset={insets.bottom}
backdropComponent={renderBackdrop}
handleIndicatorStyle={{ backgroundColor: Palette.white }}
backgroundStyle={{ backgroundColor: 'transparent' }}
onDismiss={() => {
setProjectId(null);
setTyping(false);
setText('');
}}>
setProjectId(null)
setTyping(false)
setText('')
}}
>
<BlurView
intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{
@@ -238,16 +217,18 @@ const CommentsBottomSheet = () => {
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={(insets?.bottom || 0) + 36}>
keyboardVerticalOffset={(insets?.bottom || 0) + 36}
>
<View style={{ paddingTop: 10, paddingBottom: 8 }}>
<Pressable
onPress={() => modalRef.current?.dismiss?.()}
style={{ position: 'absolute', right: 10, top: 8, padding: 6 }}
hitSlop={8}>
hitSlop={8}
>
<Image
source={icons.close}
style={{ width: 22, height: 22, tintColor: Palette.white }}
resizeMode='contain'
resizeMode="contain"
/>
</Pressable>
<Text
@@ -256,19 +237,20 @@ const CommentsBottomSheet = () => {
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
alignSelf: 'center',
}}>
}}
>
Commentaires
</Text>
</View>
<View style={{ flex: 1 }}>
<BottomSheetScrollView
ref={scrollRef}
keyboardShouldPersistTaps='handled'
keyboardDismissMode='interactive'
keyboardShouldPersistTaps="handled"
keyboardDismissMode="interactive"
onContentSizeChange={() => {
if (typing) {
try {
scrollRef.current?.scrollToEnd?.({ animated: true });
scrollRef.current?.scrollToEnd?.({ animated: true })
} catch {}
}
}}
@@ -278,12 +260,11 @@ const CommentsBottomSheet = () => {
paddingHorizontal: 14,
paddingBottom: bottomPadding,
}}
showsVerticalScrollIndicator>
showsVerticalScrollIndicator
>
{Array.isArray(comments) && comments.length > 0 ? (
comments.map((c) => (
<View
key={c.id}
style={{ flexDirection: 'row', gap: 12, marginBottom: 14 }}>
<View key={c.id} style={{ flexDirection: 'row', gap: 12, marginBottom: 14 }}>
<ProfilePicture uri={c?.profilePicture || null} size={42} />
<View style={{ flex: 1 }}>
<View
@@ -291,16 +272,16 @@ const CommentsBottomSheet = () => {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
}}>
}}
>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
}}>
{c?.userId === currentUID
? 'vous'
: c?.userName || ''}
}}
>
{c?.userId === currentUID ? 'vous' : c?.userName || ''}
</Text>
<Text
style={{
@@ -308,7 +289,8 @@ const CommentsBottomSheet = () => {
color: Palette.white,
opacity: 0.75,
fontFamily: FONT_FAMILY.InterRegular,
}}>
}}
>
{formatRelativeTime(c?.createdAt)}
</Text>
</View>
@@ -317,7 +299,8 @@ const CommentsBottomSheet = () => {
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}>
}}
>
{c?.text}
</Text>
</View>
@@ -332,7 +315,8 @@ const CommentsBottomSheet = () => {
fontFamily: FONT_FAMILY.InterRegular,
textAlign: 'center',
marginTop: 6,
}}>
}}
>
Aucun commentaire pour le moment
</Text>
)}
@@ -344,7 +328,8 @@ const CommentsBottomSheet = () => {
left: 14,
right: 14,
bottom: (insets?.bottom || 0) + INPUT_MARGIN_VERTICAL,
}}>
}}
>
<BlurView
intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{
@@ -362,25 +347,25 @@ const CommentsBottomSheet = () => {
// }
>
<BottomSheetTextInput
placeholder='Ajouter un commentaire'
placeholderTextColor='#FFFFFFAA'
placeholder="Ajouter un commentaire"
placeholderTextColor="#FFFFFFAA"
value={text}
onChangeText={(t) => {
if (!currentUID && !requireAuth()) {
return;
return
}
setText(t);
setText(t)
}}
onFocus={() => {
if (!requireAuth()) {
setTyping(false);
return;
setTyping(false)
return
}
setTyping(true);
setTyping(true)
}}
onPressIn={() => {
if (!requireAuth()) {
setTyping(false);
setTyping(false)
}
}}
onBlur={() => setTyping(false)}
@@ -395,7 +380,7 @@ const CommentsBottomSheet = () => {
<Image
source={icons.send}
style={{ width: 24, height: 24, tintColor: Palette.white }}
resizeMode='contain'
resizeMode="contain"
/>
</Pressable>
</BlurView>
@@ -404,7 +389,7 @@ const CommentsBottomSheet = () => {
</KeyboardAvoidingView>
</BlurView>
</BottomSheetModal>
);
};
)
}
export default CommentsBottomSheet;
export default CommentsBottomSheet
+28 -32
View File
@@ -1,39 +1,35 @@
import React from "react";
import { Text, useWindowDimensions, View } from "react-native";
import { Image } from "expo-image";
import { BlurView } from "expo-blur";
import { gutters, Palette } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import FontAwesome from "@expo/vector-icons/FontAwesome";
import palette from "../../../styles/Palette";
import React from 'react'
import { Text, useWindowDimensions, View } from 'react-native'
import { Image } from 'expo-image'
import { BlurView } from 'expo-blur'
import { gutters, Palette } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
import FontAwesome from '@expo/vector-icons/FontAwesome'
import palette from '../../../styles/Palette'
export default function PersonaCard({ item, index, isLock = false, height }) {
const isImageOnLeft = index % 2 === 0;
const { height: windowHeight } = useWindowDimensions();
const cardHeight = Math.max(height ?? windowHeight, 1);
const isImageOnLeft = index % 2 === 0
const { height: windowHeight } = useWindowDimensions()
const cardHeight = Math.max(height ?? windowHeight, 1)
return (
<View
style={{
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
height: cardHeight,
width: "100%",
width: '100%',
}}
>
<View
style={{
borderRadius: 10,
alignItems: "center",
justifyContent: "center",
width: "100%",
alignItems: 'center',
justifyContent: 'center',
width: '100%',
}}
>
<Image
source={item.image}
style={{ height: 190, width: "100%" }}
contentFit="contain"
/>
<Image source={item.image} style={{ height: 190, width: '100%' }} contentFit="contain" />
<BlurView
intensity={30}
tint="dark"
@@ -43,8 +39,8 @@ export default function PersonaCard({ item, index, isLock = false, height }) {
borderTopRightRadius: isImageOnLeft ? 0 : 20,
paddingVertical: 20,
paddingHorizontal: gutters,
overflow: "hidden",
width: "100%",
overflow: 'hidden',
width: '100%',
}}
>
<Text
@@ -71,7 +67,7 @@ export default function PersonaCard({ item, index, isLock = false, height }) {
<View
pointerEvents="none"
style={{
position: "absolute",
position: 'absolute',
left: 0,
right: 0,
top: 0,
@@ -79,9 +75,9 @@ export default function PersonaCard({ item, index, isLock = false, height }) {
borderRadius: 20,
borderTopLeftRadius: isImageOnLeft ? 20 : 0,
borderTopRightRadius: isImageOnLeft ? 0 : 20,
justifyContent: "center",
alignItems: "center",
backgroundColor: "rgba(0,0,0,0.2)",
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.2)',
}}
>
<View
@@ -89,9 +85,9 @@ export default function PersonaCard({ item, index, isLock = false, height }) {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: "rgba(0,0,0,0.4)",
alignItems: "center",
justifyContent: "center",
backgroundColor: 'rgba(0,0,0,0.4)',
alignItems: 'center',
justifyContent: 'center',
}}
>
<FontAwesome name="lock" size={24} color={palette.primary} />
@@ -101,5 +97,5 @@ export default function PersonaCard({ item, index, isLock = false, height }) {
</BlurView>
</View>
</View>
);
)
}
@@ -1,22 +1,22 @@
import React from "react";
import { Text, useWindowDimensions, View, StyleSheet } from "react-native";
import { Image } from "expo-image";
import { BlurView } from "expo-blur";
import { Palette } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import FontAwesome from "@expo/vector-icons/FontAwesome";
import palette from "../../../styles/Palette";
import React from 'react'
import { Text, useWindowDimensions, View, StyleSheet } from 'react-native'
import { Image } from 'expo-image'
import { BlurView } from 'expo-blur'
import { Palette } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
import FontAwesome from '@expo/vector-icons/FontAwesome'
import palette from '../../../styles/Palette'
export default function PersonaCard({ item, index, isLock = true }) {
const isImageOnLeft = index % 2 === 0;
const { height: windowHeight } = useWindowDimensions();
const isImageOnLeft = index % 2 === 0
const { height: windowHeight } = useWindowDimensions()
return (
<View
style={{
width: "100%",
justifyContent: "center",
alignItems: "center",
width: '100%',
justifyContent: 'center',
alignItems: 'center',
height: windowHeight,
}}
>
@@ -24,21 +24,15 @@ export default function PersonaCard({ item, index, isLock = true }) {
style={{
padding: 10,
borderRadius: 10,
backgroundColor: isLock
? "rgba(0, 0, 0, 0.2)"
: "rgba(0, 0, 0, 0.05)",
alignItems: "center",
justifyContent: "center",
width: "90%",
height: "30%",
flexDirection: isImageOnLeft ? "row" : "row-reverse",
backgroundColor: isLock ? 'rgba(0, 0, 0, 0.2)' : 'rgba(0, 0, 0, 0.05)',
alignItems: 'center',
justifyContent: 'center',
width: '90%',
height: '30%',
flexDirection: isImageOnLeft ? 'row' : 'row-reverse',
}}
>
<Image
source={item.image}
style={{ height: 250, width: 400 }}
contentFit="contain"
/>
<Image source={item.image} style={{ height: 250, width: 400 }} contentFit="contain" />
<BlurView
intensity={30}
tint="dark"
@@ -50,14 +44,14 @@ export default function PersonaCard({ item, index, isLock = true }) {
borderBottomRightRadius: isImageOnLeft ? 20 : 0,
paddingHorizontal: 18,
paddingVertical: 16,
overflow: "hidden",
overflow: 'hidden',
}}
>
{isLock && (
<View
style={{
...StyleSheet.absoluteFillObject,
backgroundColor: "rgba(0,0,0,0.6)",
backgroundColor: 'rgba(0,0,0,0.6)',
}}
/>
)}
@@ -80,7 +74,7 @@ export default function PersonaCard({ item, index, isLock = true }) {
style={{
fontSize: 14,
lineHeight: 20,
color: "rgba(255, 255, 255, 0.7)",
color: 'rgba(255, 255, 255, 0.7)',
fontFamily: FONT_FAMILY.InterRegular,
}}
>
@@ -90,14 +84,14 @@ export default function PersonaCard({ item, index, isLock = true }) {
<View
pointerEvents="none"
style={{
position: "absolute",
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
borderRadius: 20,
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
}}
>
<View
@@ -105,9 +99,9 @@ export default function PersonaCard({ item, index, isLock = true }) {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: "rgba(0,0,0,0.4)",
alignItems: "center",
justifyContent: "center",
backgroundColor: 'rgba(0,0,0,0.4)',
alignItems: 'center',
justifyContent: 'center',
}}
>
<FontAwesome name="lock" size={24} color={palette.primary} />
@@ -118,5 +112,5 @@ export default function PersonaCard({ item, index, isLock = true }) {
</BlurView>
</View>
</View>
);
)
}
+190 -255
View File
@@ -1,4 +1,4 @@
import React from "react";
import React from 'react'
import {
ActivityIndicator,
Modal,
@@ -7,136 +7,119 @@ import {
StyleSheet,
Text,
View,
} from "react-native";
import { BlurView } from "expo-blur";
import BorderGradientButton from "../BorderGradientButton";
import GradientButton from "../GradientButton";
import CreateLyricsHeader from "../../screens/Writing/components/CreateLyricsHeader";
import { Palette, gutters } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { isWeb } from "../../hooks/useLayoutType";
import CreditAmount from "../CreditAmount";
import { useStripe } from "../../providers/StripeProvider";
} from 'react-native'
import { BlurView } from 'expo-blur'
import BorderGradientButton from '../BorderGradientButton'
import GradientButton from '../GradientButton'
import CreateLyricsHeader from '../../screens/Writing/components/CreateLyricsHeader'
import { Palette, gutters } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { isWeb } from '../../hooks/useLayoutType'
import CreditAmount from '../CreditAmount'
import { useStripe } from '../../providers/StripeProvider'
const WEB_MODAL_MAX_WIDTH = 820;
const formatCurrency = (amount, currency = "eur") => {
if (typeof amount !== "number") {
return null;
const WEB_MODAL_MAX_WIDTH = 820
const formatCurrency = (amount, currency = 'eur') => {
if (typeof amount !== 'number') {
return null
}
const normalized = amount / 100;
const normalized = amount / 100
const upperCurrency =
typeof currency === "string" && currency.trim()
? currency.trim().toUpperCase()
: "EUR";
typeof currency === 'string' && currency.trim() ? currency.trim().toUpperCase() : 'EUR'
if (typeof Intl !== "undefined" && Intl.NumberFormat) {
if (typeof Intl !== 'undefined' && Intl.NumberFormat) {
try {
return new Intl.NumberFormat("fr-FR", {
style: "currency",
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: upperCurrency,
minimumFractionDigits: 2,
}).format(normalized);
}).format(normalized)
} catch (_error) {}
}
return `${normalized.toFixed(2)} ${upperCurrency}`;
};
return `${normalized.toFixed(2)} ${upperCurrency}`
}
const FALLBACK_BASE_PRICE_PER_COIN = 12; // ~0,12€ par jeton (valeur indicatif pour l'affichage)
const FALLBACK_DISCOUNT_STEPS = [0, 12, 18, 26, 32];
const STATIC_DISCOUNTS = [0, 30, 50];
const FALLBACK_BASE_PRICE_PER_COIN = 12 // ~0,12€ par jeton (valeur indicatif pour l'affichage)
const FALLBACK_DISCOUNT_STEPS = [0, 12, 18, 26, 32]
const STATIC_DISCOUNTS = [0, 30, 50]
const computeCoinPackPricing = (packs = []) => {
if (!Array.isArray(packs) || !packs.length) {
return {};
return {}
}
const packsWithPrice = packs
.filter((pack) => pack?.productId)
.map((pack) => {
const hasValidPrice =
typeof pack.unitAmount === "number" &&
typeof pack.coinAmount === "number" &&
pack.coinAmount > 0;
typeof pack.unitAmount === 'number' &&
typeof pack.coinAmount === 'number' &&
pack.coinAmount > 0
return {
...pack,
hasValidPrice,
pricePerCoin: hasValidPrice ? pack.unitAmount / pack.coinAmount : null,
};
});
}
})
const basePack = packsWithPrice.reduce((current, pack) => {
if (!pack.hasValidPrice) {
return current;
return current
}
if (!current) {
return pack;
return pack
}
if (
typeof pack.coinAmount === "number" &&
typeof current.coinAmount === "number" &&
typeof pack.coinAmount === 'number' &&
typeof current.coinAmount === 'number' &&
pack.coinAmount < current.coinAmount
) {
return pack;
return pack
}
return current;
}, null);
return current
}, null)
const basePricePerCoin =
basePack?.pricePerCoin && isFinite(basePack.pricePerCoin)
? basePack.pricePerCoin
: null;
basePack?.pricePerCoin && isFinite(basePack.pricePerCoin) ? basePack.pricePerCoin : null
const bestDiscountPack = packsWithPrice.reduce((best, pack) => {
if (!pack.hasValidPrice || !basePricePerCoin) {
return best;
return best
}
const discount =
((basePricePerCoin - pack.pricePerCoin) / basePricePerCoin) * 100;
const discount = ((basePricePerCoin - pack.pricePerCoin) / basePricePerCoin) * 100
if (!best || discount > best.discount) {
return { productId: pack.productId, discount };
return { productId: pack.productId, discount }
}
return best;
}, null);
return best
}, null)
const fallbackBase = basePricePerCoin || FALLBACK_BASE_PRICE_PER_COIN;
const fallbackBase = basePricePerCoin || FALLBACK_BASE_PRICE_PER_COIN
const fallbackBestId =
!bestDiscountPack && packs.length
? packs[packs.length - 1]?.productId
: null;
!bestDiscountPack && packs.length ? packs[packs.length - 1]?.productId : null
return packs.reduce((acc, pack, index) => {
if (!pack?.productId) {
return acc;
return acc
}
const currentPack = packsWithPrice.find(
(item) => item.productId === pack.productId,
);
const computedPricePerCoin = currentPack?.pricePerCoin;
const currentPack = packsWithPrice.find((item) => item.productId === pack.productId)
const computedPricePerCoin = currentPack?.pricePerCoin
const fallbackDiscount =
FALLBACK_DISCOUNT_STEPS[
Math.min(index, FALLBACK_DISCOUNT_STEPS.length - 1)
] || 0;
FALLBACK_DISCOUNT_STEPS[Math.min(index, FALLBACK_DISCOUNT_STEPS.length - 1)] || 0
const pricePerCoin =
typeof computedPricePerCoin === "number" && isFinite(computedPricePerCoin)
typeof computedPricePerCoin === 'number' && isFinite(computedPricePerCoin)
? computedPricePerCoin
: fallbackBase * (1 - fallbackDiscount / 100);
: fallbackBase * (1 - fallbackDiscount / 100)
const baseReference = fallbackBase || 1;
const baseReference = fallbackBase || 1
const rawDiscount =
baseReference && pricePerCoin
? ((baseReference - pricePerCoin) / baseReference) * 100
: 0;
const discountPercent = Math.max(
0,
Number.isFinite(rawDiscount) ? Math.round(rawDiscount) : 0,
);
baseReference && pricePerCoin ? ((baseReference - pricePerCoin) / baseReference) * 100 : 0
const discountPercent = Math.max(0, Number.isFinite(rawDiscount) ? Math.round(rawDiscount) : 0)
const isBasePack =
(basePack && basePack.productId === pack.productId) ||
(!basePack && index === 0);
(basePack && basePack.productId === pack.productId) || (!basePack && index === 0)
acc[pack.productId] = {
currency: pack.currency,
@@ -147,43 +130,35 @@ const computeCoinPackPricing = (packs = []) => {
isBestValue:
(bestDiscountPack && bestDiscountPack.productId === pack.productId) ||
(!bestDiscountPack && fallbackBestId === pack.productId),
};
return acc;
}, {});
};
function CoinPackCard({
pack,
selected,
pricingDetail,
onSelect,
staticDiscountPercent,
}) {
const handleSelect = React.useCallback(() => {
if (typeof onSelect !== "function" || !pack?.productId) {
return;
}
onSelect(pack.productId);
}, [onSelect, pack?.productId]);
const formattedPrice = formatCurrency(pack?.unitAmount, pack?.currency);
const perCoinPrice = pricingDetail?.formattedPricePerCoin;
const discountPercent = pricingDetail?.discountPercent;
const hasStaticDiscount = typeof staticDiscountPercent === "number";
const effectiveDiscountPercent = hasStaticDiscount
? staticDiscountPercent
: discountPercent;
const isBaseReference = !hasStaticDiscount && pricingDetail?.isBasePack;
return acc
}, {})
}
function CoinPackCard({ pack, selected, pricingDetail, onSelect, staticDiscountPercent }) {
const handleSelect = React.useCallback(() => {
if (typeof onSelect !== 'function' || !pack?.productId) {
return
}
onSelect(pack.productId)
}, [onSelect, pack?.productId])
const formattedPrice = formatCurrency(pack?.unitAmount, pack?.currency)
const perCoinPrice = pricingDetail?.formattedPricePerCoin
const discountPercent = pricingDetail?.discountPercent
const hasStaticDiscount = typeof staticDiscountPercent === 'number'
const effectiveDiscountPercent = hasStaticDiscount ? staticDiscountPercent : discountPercent
const isBaseReference = !hasStaticDiscount && pricingDetail?.isBasePack
const discountLabel = hasStaticDiscount
? staticDiscountPercent > 0
? `-${staticDiscountPercent}%`
: null
: isBaseReference
? "Pack de base (référence)"
: typeof discountPercent === "number"
? 'Pack de base (référence)'
: typeof discountPercent === 'number'
? `-${discountPercent}% vs pack de base`
: null;
: null
return (
<Pressable
@@ -212,17 +187,12 @@ function CoinPackCard({
{discountLabel ? (
<View
style={{
position: "absolute",
position: 'absolute',
right: 0,
top: -5,
}}
>
<View
style={[
styles.discountBadge,
isBaseReference && styles.discountBadgeBase,
]}
>
<View style={[styles.discountBadge, isBaseReference && styles.discountBadgeBase]}>
<Text style={[styles.discountText]}>{discountLabel}</Text>
</View>
</View>
@@ -230,103 +200,83 @@ function CoinPackCard({
{pack?.name ? <Text style={styles.packName}>{pack.name}</Text> : null}
</View>
<View style={styles.priceBlock}>
{formattedPrice ? (
<Text style={styles.packPrice}>{formattedPrice}</Text>
) : null}
{formattedPrice ? <Text style={styles.packPrice}>{formattedPrice}</Text> : null}
</View>
</BlurView>
</Pressable>
);
)
}
const CoinPackModal = ({ visible, onClose }) => {
const [selectedPackId, setSelectedPackId] = React.useState(null);
const [isProcessing, setIsProcessing] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState(null);
const modalMaxWidth = isWeb ? WEB_MODAL_MAX_WIDTH : undefined;
const [selectedPackId, setSelectedPackId] = React.useState(null)
const [isProcessing, setIsProcessing] = React.useState(false)
const [errorMessage, setErrorMessage] = React.useState(null)
const modalMaxWidth = isWeb ? WEB_MODAL_MAX_WIDTH : undefined
const {
coinPacks,
isCatalogLoading,
catalogError,
refreshCatalog,
createCoinPackCheckout,
} = useStripe();
const packPricingById = React.useMemo(
() => computeCoinPackPricing(coinPacks),
[coinPacks],
);
const { coinPacks, isCatalogLoading, catalogError, refreshCatalog, createCoinPackCheckout } =
useStripe()
const packPricingById = React.useMemo(() => computeCoinPackPricing(coinPacks), [coinPacks])
React.useEffect(() => {
if (!coinPacks.length) {
setSelectedPackId(null);
return;
setSelectedPackId(null)
return
}
setSelectedPackId((current) => {
if (
current &&
coinPacks.some((pack) => pack?.productId && pack.productId === current)
) {
return current;
if (current && coinPacks.some((pack) => pack?.productId && pack.productId === current)) {
return current
}
return coinPacks[0]?.productId || null;
});
}, [coinPacks]);
return coinPacks[0]?.productId || null
})
}, [coinPacks])
React.useEffect(() => {
if (!visible) {
return;
return
}
if (!coinPacks.length && !isCatalogLoading && !catalogError) {
refreshCatalog();
refreshCatalog()
}
}, [
visible,
coinPacks.length,
isCatalogLoading,
catalogError,
refreshCatalog,
]);
}, [visible, coinPacks.length, isCatalogLoading, catalogError, refreshCatalog])
const closeModal = React.useCallback(
({ force = false } = {}) => {
if (isProcessing && !force) {
return;
return
}
setIsProcessing(false);
onClose?.();
setIsProcessing(false)
onClose?.()
},
[isProcessing, onClose],
);
[isProcessing, onClose]
)
const handleCheckout = React.useCallback(async () => {
if (!selectedPackId) {
return;
return
}
setIsProcessing(true);
setErrorMessage(null);
setIsProcessing(true)
setErrorMessage(null)
try {
await createCoinPackCheckout(selectedPackId);
closeModal({ force: true });
await createCoinPackCheckout(selectedPackId)
closeModal({ force: true })
} catch (error) {
console.error("[CoinPackModal] checkout error", error);
console.error('[CoinPackModal] checkout error', error)
setErrorMessage(
error?.message ||
"Une erreur est survenue lors de la création de la session Stripe.",
);
error?.message || 'Une erreur est survenue lors de la création de la session Stripe.'
)
} finally {
setIsProcessing(false);
setIsProcessing(false)
}
}, [selectedPackId, createCoinPackCheckout, closeModal]);
}, [selectedPackId, createCoinPackCheckout, closeModal])
const handleClose = React.useCallback(() => {
closeModal();
}, [closeModal]);
closeModal()
}, [closeModal])
const isLoadingCoinPacks = isCatalogLoading && !coinPacks.length;
const combinedErrorMessage = errorMessage || catalogError;
const isLoadingCoinPacks = isCatalogLoading && !coinPacks.length
const combinedErrorMessage = errorMessage || catalogError
const renderContent = () => {
if (isLoadingCoinPacks) {
@@ -334,15 +284,13 @@ const CoinPackModal = ({ visible, onClose }) => {
<View style={styles.loaderContainer}>
<ActivityIndicator color={Palette.white} />
</View>
);
)
}
if (!coinPacks.length) {
return (
<Text style={styles.emptyState}>
Aucun pack de pièces n'est disponible pour le moment.
</Text>
);
<Text style={styles.emptyState}>Aucun pack de pièces n'est disponible pour le moment.</Text>
)
}
return (
@@ -364,25 +312,20 @@ const CoinPackModal = ({ visible, onClose }) => {
/>
))}
</ScrollView>
);
};
)
}
return (
<Modal
animationType="slide"
transparent
visible={visible}
onRequestClose={handleClose}
>
<Modal animationType="slide" transparent visible={visible} onRequestClose={handleClose}>
<View style={styles.overlay}>
<CreateLyricsHeader
containerStyle={{
width: "100%",
width: '100%',
maxWidth: modalMaxWidth,
alignSelf: "center",
alignSelf: 'center',
borderWidth: isWeb ? 1 : 0,
borderColor: "rgba(255,255,255,0.16)",
backgroundColor: isWeb ? "rgba(12, 10, 18, 0.85)" : undefined,
borderColor: 'rgba(255,255,255,0.16)',
backgroundColor: isWeb ? 'rgba(12, 10, 18, 0.85)' : undefined,
}}
>
<View style={styles.container}>
@@ -400,27 +343,19 @@ const CoinPackModal = ({ visible, onClose }) => {
<View style={styles.content}>{renderContent()}</View>
<Text style={styles.indicativeNote}>
Tarifs indicatifs : les prix et quantités de jetons sont amenés à
évoluer. Les remises sont affichées vs le pack de base pour mieux
valoriser les offres volumineuses.
Tarifs indicatifs : les prix et quantités de jetons sont amenés à évoluer. Les remises
sont affichées vs le pack de base pour mieux valoriser les offres volumineuses.
</Text>
<View style={styles.actions}>
<GradientButton
title={isProcessing ? "Redirection..." : "Acheter ce pack"}
title={isProcessing ? 'Redirection...' : 'Acheter ce pack'}
onPress={handleCheckout}
disabled={
!selectedPackId ||
isProcessing ||
isLoadingCoinPacks ||
!coinPacks.length
!selectedPackId || isProcessing || isLoadingCoinPacks || !coinPacks.length
}
/>
<BorderGradientButton
title="Fermer"
onPress={handleClose}
disabled={isProcessing}
/>
<BorderGradientButton title="Fermer" onPress={handleClose} disabled={isProcessing} />
</View>
<Text style={styles.disclaimer}>
@@ -430,81 +365,81 @@ const CoinPackModal = ({ visible, onClose }) => {
</CreateLyricsHeader>
</View>
</Modal>
);
};
)
}
export default CoinPackModal;
export default CoinPackModal
const styles = StyleSheet.create({
overlay: {
flex: 1,
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: gutters,
paddingVertical: gutters * 1.5,
},
container: {
gap: 24,
alignItems: "center",
alignItems: 'center',
paddingBottom: gutters,
paddingHorizontal: isWeb ? gutters * 1.5 : 0,
width: "100%",
width: '100%',
maxWidth: isWeb ? WEB_MODAL_MAX_WIDTH : undefined,
},
header: {
gap: 8,
alignItems: "center",
alignItems: 'center',
paddingHorizontal: 16,
},
title: {
fontFamily: FONT_FAMILY.InterSemiBold,
fontSize: 22,
color: Palette.white,
textAlign: "center",
textAlign: 'center',
},
subtitle: {
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 16,
color: Palette.white,
textAlign: "center",
textAlign: 'center',
},
errorText: {
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 14,
color: Palette.red,
textAlign: "center",
textAlign: 'center',
paddingHorizontal: 16,
},
content: {
width: "100%",
maxWidth: isWeb ? WEB_MODAL_MAX_WIDTH : "100%",
alignSelf: "center",
width: '100%',
maxWidth: isWeb ? WEB_MODAL_MAX_WIDTH : '100%',
alignSelf: 'center',
maxHeight: isWeb ? 420 : 360,
},
loaderContainer: {
width: "100%",
alignItems: "center",
justifyContent: "center",
width: '100%',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 32,
},
emptyState: {
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 14,
color: "rgba(255, 255, 255, 0.72)",
textAlign: "center",
color: 'rgba(255, 255, 255, 0.72)',
textAlign: 'center',
paddingHorizontal: 24,
},
packList: {
width: "100%",
maxWidth: isWeb ? WEB_MODAL_MAX_WIDTH : "100%",
alignSelf: "center",
width: '100%',
maxWidth: isWeb ? WEB_MODAL_MAX_WIDTH : '100%',
alignSelf: 'center',
gap: gutters,
paddingHorizontal: isWeb ? gutters * 1.5 : 0,
},
packListWeb: {
flexDirection: "row",
justifyContent: "center",
flexWrap: "wrap",
flexDirection: 'row',
justifyContent: 'center',
flexWrap: 'wrap',
},
packListMobile: {
paddingBottom: 12,
@@ -512,15 +447,15 @@ const styles = StyleSheet.create({
cardWrapper: {
flex: 1,
borderRadius: 20,
overflow: "hidden",
overflow: 'hidden',
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.12)",
backgroundColor: "rgba(12, 14, 18, 0.45)",
borderColor: 'rgba(255, 255, 255, 0.12)',
backgroundColor: 'rgba(12, 14, 18, 0.45)',
minWidth: isWeb ? 220 : undefined,
},
cardWrapperSelected: {
borderColor: Palette.primary,
shadowColor: "#000",
shadowColor: '#000',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.3,
shadowRadius: 18,
@@ -534,16 +469,16 @@ const styles = StyleSheet.create({
gap: 16,
paddingVertical: 20,
paddingHorizontal: 24,
justifyContent: "space-between",
backgroundColor: "rgba(48, 52, 56, 0.55)",
justifyContent: 'space-between',
backgroundColor: 'rgba(48, 52, 56, 0.55)',
},
cardHeader: {
gap: 12,
alignItems: "center",
alignItems: 'center',
},
coinRow: {
flexDirection: "row",
alignItems: "center",
flexDirection: 'row',
alignItems: 'center',
gap: 10,
},
coinAmount: {
@@ -555,37 +490,37 @@ const styles = StyleSheet.create({
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 16,
color: Palette.white,
textAlign: "center",
textAlign: 'center',
},
packDescription: {
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 14,
color: "rgba(255, 255, 255, 0.72)",
textAlign: "center",
color: 'rgba(255, 255, 255, 0.72)',
textAlign: 'center',
},
priceBlock: {
gap: 2,
alignItems: "center",
alignItems: 'center',
},
packPrice: {
fontFamily: FONT_FAMILY.InterSemiBold,
fontSize: 18,
color: Palette.white,
textAlign: "center",
textAlign: 'center',
},
pricePerCoin: {
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 13,
color: "rgba(255, 255, 255, 0.7)",
textAlign: "center",
color: 'rgba(255, 255, 255, 0.7)',
textAlign: 'center',
},
discountBadge: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 999,
backgroundColor: "rgba(255, 255, 255, 0.06)",
backgroundColor: 'rgba(255, 255, 255, 0.06)',
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.12)",
borderColor: 'rgba(255, 255, 255, 0.12)',
},
discountBadgeBase: {
borderColor: Palette.primary,
@@ -599,7 +534,7 @@ const styles = StyleSheet.create({
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 13,
color: Palette.white,
textAlign: "center",
textAlign: 'center',
},
discountTextBest: {
color: Palette.white,
@@ -607,22 +542,22 @@ const styles = StyleSheet.create({
discountHelper: {
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 12,
color: "rgba(255, 255, 255, 0.72)",
textAlign: "center",
color: 'rgba(255, 255, 255, 0.72)',
textAlign: 'center',
},
discountHelperMuted: {
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 12,
color: "rgba(255, 255, 255, 0.56)",
textAlign: "center",
color: 'rgba(255, 255, 255, 0.56)',
textAlign: 'center',
},
tagBestValue: {
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 10,
backgroundColor: Palette.transparentGreen,
alignSelf: "center",
position: "absolute",
alignSelf: 'center',
position: 'absolute',
top: 10,
},
tagBestValueText: {
@@ -631,22 +566,22 @@ const styles = StyleSheet.create({
color: Palette.white,
},
actions: {
width: "85%",
alignSelf: "center",
width: '85%',
alignSelf: 'center',
gap: 16,
},
disclaimer: {
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 13,
color: "rgba(255, 255, 255, 0.7)",
textAlign: "center",
color: 'rgba(255, 255, 255, 0.7)',
textAlign: 'center',
paddingHorizontal: 16,
},
indicativeNote: {
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 12,
color: "rgba(255, 255, 255, 0.62)",
textAlign: "center",
color: 'rgba(255, 255, 255, 0.62)',
textAlign: 'center',
paddingHorizontal: 24,
},
});
})
+47 -47
View File
@@ -1,68 +1,68 @@
import { View, Text } from "react-native";
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";
import { View, Text } from 'react-native'
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 [currentUID] = useGlobal('currentUID')
const [, setTooltip] = useGlobal('_tooltip')
const onClose = async () => {
await SheetManager.hide("DeleteAccount");
};
await SheetManager.hide('DeleteAccount')
}
const confirmDelete = async () => {
await onClose().catch(() => {});
await onClose().catch(() => {})
alert(
"Êtes-vous sûr ?",
"Cette action supprimera votre profil.",
'Êtes-vous sûr ?',
'Cette action supprimera votre profil.',
[
{ text: "Annuler", style: "cancel", onPress: () => {} },
{ text: 'Annuler', style: 'cancel', onPress: () => {} },
{
text: "Confirmer",
text: 'Confirmer',
onPress: () => {
alert(
"Confirmation",
"Dernière vérification : souhaitez-vous supprimer définitivement votre profil ?",
'Confirmation',
'Dernière vérification : souhaitez-vous supprimer définitivement votre profil ?',
[
{ text: "Non", style: "cancel", onPress: () => {} },
{ text: 'Non', style: 'cancel', onPress: () => {} },
{
text: "Oui, supprimer",
text: 'Oui, supprimer',
onPress: async () => {
try {
if (!currentUID) return;
await usersRef.doc(currentUID).delete();
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é" });
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",
});
type: 'error',
text: e?.message || 'Suppression impossible',
})
} finally {
onClose();
onClose()
}
},
},
],
{ cancelable: true },
);
{ cancelable: true }
)
},
},
],
{ cancelable: true },
);
};
{ cancelable: true }
)
}
return (
<AppActionSheet id="DeleteAccount">
@@ -73,7 +73,7 @@ const DeleteAccountModal = () => {
fontSize: 20,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueMedium,
textAlign: "center",
textAlign: 'center',
}}
>
Supprimer mon compte
@@ -83,20 +83,20 @@ const DeleteAccountModal = () => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
textAlign: "center",
textAlign: 'center',
}}
>
Es-tu sûr de vouloir supprimer ton compte ?{"\n"}Attention, cette
action est irréversible !
Es-tu sûr de vouloir supprimer ton compte ?{'\n'}Attention, cette action est
irréversible !
</Text>
</View>
<View style={{ width: "80%", alignSelf: "center", gap: 12 }}>
<View style={{ width: '80%', alignSelf: 'center', gap: 12 }}>
<BorderGradientButton title="Oui, supprimer" onPress={confirmDelete} />
<GradientButton title="Non, annuler" onPress={onClose} />
</View>
</View>
</AppActionSheet>
);
};
)
}
export default DeleteAccountModal;
export default DeleteAccountModal
+17 -17
View File
@@ -1,16 +1,16 @@
import { View, Text } from "react-native";
import React from "react";
import AppActionSheet from "../AppActionSheet";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import BorderGradientButton from "../BorderGradientButton";
import GradientButton from "../GradientButton";
import { SheetManager } from "react-native-actions-sheet";
import { View, Text } from 'react-native'
import React from 'react'
import AppActionSheet from '../AppActionSheet'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import BorderGradientButton from '../BorderGradientButton'
import GradientButton from '../GradientButton'
import { SheetManager } from 'react-native-actions-sheet'
const DeleteAudioModal = () => {
const onClose = () => {
SheetManager.hide("DeleteAudio");
};
SheetManager.hide('DeleteAudio')
}
return (
<AppActionSheet id="DeleteAudio">
@@ -21,7 +21,7 @@ const DeleteAudioModal = () => {
fontSize: 20,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueMedium,
textAlign: "center",
textAlign: 'center',
}}
>
Supprimer mon audio
@@ -31,20 +31,20 @@ const DeleteAudioModal = () => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
textAlign: "center",
textAlign: 'center',
}}
>
Es-tu sur de vouloir supprimer ton audio?{"\n"}
Es-tu sur de vouloir supprimer ton audio?{'\n'}
Attention, cette action est irréversible!
</Text>
</View>
<View style={{ width: "80%", alignSelf: "center", gap: 12 }}>
<View style={{ width: '80%', alignSelf: 'center', gap: 12 }}>
<BorderGradientButton title="Oui, supprimer" onPress={onClose} />
<GradientButton title="Non, annuler" onPress={onClose} />
</View>
</View>
</AppActionSheet>
);
};
)
}
export default DeleteAudioModal;
export default DeleteAudioModal
+23 -26
View File
@@ -1,29 +1,29 @@
import React from "react";
import { Text, View } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import AppActionSheet from "../AppActionSheet";
import BorderGradientButton from "../BorderGradientButton";
import GradientButton from "../GradientButton";
import React from 'react'
import { Text, View } from 'react-native'
import { SheetManager } from 'react-native-actions-sheet'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import AppActionSheet from '../AppActionSheet'
import BorderGradientButton from '../BorderGradientButton'
import GradientButton from '../GradientButton'
const DeleteModal = ({ payload }) => {
const {
title = "Supprimer ma playlist",
message = "Es-tu sur de vouloir supprimer ta playlist?",
title = 'Supprimer ma playlist',
message = 'Es-tu sur de vouloir supprimer ta playlist?',
onConfirm,
} = payload || {};
} = payload || {}
const onClose = () => {
SheetManager.hide("Delete");
};
SheetManager.hide('Delete')
}
const handleConfirm = async () => {
if (onConfirm) {
await onConfirm();
await onConfirm()
}
onClose();
};
onClose()
}
return (
<AppActionSheet id="Delete">
@@ -34,7 +34,7 @@ const DeleteModal = ({ payload }) => {
fontSize: 20,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueMedium,
textAlign: "center",
textAlign: 'center',
}}
>
{title}
@@ -44,22 +44,19 @@ const DeleteModal = ({ payload }) => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
textAlign: "center",
textAlign: 'center',
}}
>
{message}
</Text>
</View>
<View style={{ width: "80%", alignSelf: "center", gap: 12 }}>
<BorderGradientButton
title="Oui, supprimer"
onPress={handleConfirm}
/>
<View style={{ width: '80%', alignSelf: 'center', gap: 12 }}>
<BorderGradientButton title="Oui, supprimer" onPress={handleConfirm} />
<GradientButton title="Non, annuler" onPress={onClose} />
</View>
</View>
</AppActionSheet>
);
};
)
}
export default DeleteModal;
export default DeleteModal
+17 -17
View File
@@ -1,16 +1,16 @@
import { View, Text } from "react-native";
import React from "react";
import AppActionSheet from "../AppActionSheet";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import BorderGradientButton from "../BorderGradientButton";
import GradientButton from "../GradientButton";
import { SheetManager } from "react-native-actions-sheet";
import { View, Text } from 'react-native'
import React from 'react'
import AppActionSheet from '../AppActionSheet'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import BorderGradientButton from '../BorderGradientButton'
import GradientButton from '../GradientButton'
import { SheetManager } from 'react-native-actions-sheet'
const DeletePlaybackModal = () => {
const onClose = () => {
SheetManager.hide("DeletePlayback");
};
SheetManager.hide('DeletePlayback')
}
return (
<AppActionSheet id="DeletePlayback">
@@ -21,7 +21,7 @@ const DeletePlaybackModal = () => {
fontSize: 20,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueMedium,
textAlign: "center",
textAlign: 'center',
}}
>
Supprimer mon playback
@@ -31,20 +31,20 @@ const DeletePlaybackModal = () => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
textAlign: "center",
textAlign: 'center',
}}
>
Es-tu sur de vouloir supprimer ton playback?{"\n"}
Es-tu sur de vouloir supprimer ton playback?{'\n'}
Attention, cette action est irréversible!
</Text>
</View>
<View style={{ width: "80%", alignSelf: "center", gap: 12 }}>
<View style={{ width: '80%', alignSelf: 'center', gap: 12 }}>
<BorderGradientButton title="Oui, supprimer" onPress={onClose} />
<GradientButton title="Non, annuler" onPress={onClose} />
</View>
</View>
</AppActionSheet>
);
};
)
}
export default DeletePlaybackModal;
export default DeletePlaybackModal
+54 -62
View File
@@ -1,10 +1,10 @@
import React, { useCallback } from "react";
import { Pressable, Text, View } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { openShareSheet } from "../../utils/shareSheet";
import AppActionSheet from "../AppActionSheet";
import React, { useCallback } from 'react'
import { Pressable, Text, View } from 'react-native'
import { SheetManager } from 'react-native-actions-sheet'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { openShareSheet } from '../../utils/shareSheet'
import AppActionSheet from '../AppActionSheet'
const OptionButton = ({ label, onPress, disabled = false }) => (
<Pressable
@@ -14,7 +14,7 @@ const OptionButton = ({ label, onPress, disabled = false }) => (
paddingVertical: 14,
paddingHorizontal: 18,
borderRadius: 16,
backgroundColor: "rgba(255,255,255,0.08)",
backgroundColor: 'rgba(255,255,255,0.08)',
opacity: disabled ? 0.5 : 1,
transform: [{ scale: pressed ? 0.97 : 1 }],
})}
@@ -24,13 +24,13 @@ const OptionButton = ({ label, onPress, disabled = false }) => (
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
textAlign: "center",
textAlign: 'center',
}}
>
{label}
</Text>
</Pressable>
);
)
const MusicOptionsModal = ({ payload }) => {
const {
@@ -44,79 +44,78 @@ const MusicOptionsModal = ({ payload }) => {
onShare,
sharePayload = null,
shareDisabled = false,
} = payload || {};
} = payload || {}
const trigger = useCallback((action) => {
Promise.resolve(SheetManager.hide("MusicOptions"))
Promise.resolve(SheetManager.hide('MusicOptions'))
.catch(() => {})
.finally(() => {
if (typeof action === "function") {
setTimeout(action, 220);
if (typeof action === 'function') {
setTimeout(action, 220)
}
});
}, []);
})
}, [])
const handleReport = useCallback(() => {
const run = () => {
if (typeof onReport === "function") {
onReport();
return;
if (typeof onReport === 'function') {
onReport()
return
}
if (projectId) {
SheetManager.show("Report", {
SheetManager.show('Report', {
payload: {
targetType: "music",
targetType: 'music',
projectId,
title,
ownerId,
},
});
})
}
};
trigger(run);
}, [onReport, ownerId, projectId, title, trigger]);
}
trigger(run)
}, [onReport, ownerId, projectId, title, trigger])
const handleAddToPlaylist = useCallback(() => {
const run = () => {
if (typeof onAddToPlaylist === "function") {
onAddToPlaylist();
return;
if (typeof onAddToPlaylist === 'function') {
onAddToPlaylist()
return
}
if (projectId) {
SheetManager.show("Playlist", { payload: { projectId } });
SheetManager.show('Playlist', { payload: { projectId } })
} else {
SheetManager.show("Playlist");
SheetManager.show('Playlist')
}
};
trigger(run);
}, [onAddToPlaylist, projectId, trigger]);
}
trigger(run)
}, [onAddToPlaylist, projectId, trigger])
const handleDownload = useCallback(() => {
if (downloadDisabled) return;
if (downloadDisabled) return
const run = () => {
if (typeof onDownload === "function") {
onDownload();
if (typeof onDownload === 'function') {
onDownload()
}
};
trigger(run);
}, [downloadDisabled, onDownload, trigger]);
}
trigger(run)
}, [downloadDisabled, onDownload, trigger])
const handleShare = useCallback(() => {
if (shareDisabled) return;
if (shareDisabled) return
const run = () => {
if (typeof onShare === "function") {
onShare();
return;
if (typeof onShare === 'function') {
onShare()
return
}
if (sharePayload) {
openShareSheet(sharePayload);
openShareSheet(sharePayload)
}
};
trigger(run);
}, [onShare, shareDisabled, sharePayload, trigger]);
}
trigger(run)
}, [onShare, shareDisabled, sharePayload, trigger])
const shouldShowShareOption =
typeof onShare === "function" || !!sharePayload;
const shouldShowShareOption = typeof onShare === 'function' || !!sharePayload
return (
<AppActionSheet id="MusicOptions">
@@ -126,17 +125,14 @@ const MusicOptionsModal = ({ payload }) => {
fontSize: 20,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueMedium,
textAlign: "center",
textAlign: 'center',
}}
>
Options
</Text>
<View style={{ gap: 12 }}>
<OptionButton label="Signaler" onPress={handleReport} />
<OptionButton
label="Ajouter à une playlist"
onPress={handleAddToPlaylist}
/>
<OptionButton label="Ajouter à une playlist" onPress={handleAddToPlaylist} />
{shouldShowShareOption && (
<OptionButton
label="Partager la musique"
@@ -144,15 +140,11 @@ const MusicOptionsModal = ({ payload }) => {
disabled={shareDisabled}
/>
)}
<OptionButton
label="Télécharger"
onPress={handleDownload}
disabled={downloadDisabled}
/>
<OptionButton label="Télécharger" onPress={handleDownload} disabled={downloadDisabled} />
</View>
</View>
</AppActionSheet>
);
};
)
}
export default MusicOptionsModal;
export default MusicOptionsModal
+80 -97
View File
@@ -1,148 +1,134 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
Image,
Pressable,
ScrollView,
StyleSheet,
Text,
View,
findNodeHandle,
} from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import AppActionSheet from "../AppActionSheet";
import GradientButton from "../GradientButton";
import { img } from "../../assets";
import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService";
import { useUserData } from "../../providers/UserDataProvider";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import Style, { size } from "../../styles/Style";
import { isWeb } from "../../hooks/useLayoutType";
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Image, Pressable, ScrollView, StyleSheet, Text, View, findNodeHandle } from 'react-native'
import { SheetManager } from 'react-native-actions-sheet'
import AppActionSheet from '../AppActionSheet'
import GradientButton from '../GradientButton'
import { img } from '../../assets'
import { Routes } from '../../navigation'
import { navigate } from '../../navigation/NavigationService'
import { useUserData } from '../../providers/UserDataProvider'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import Style, { size } from '../../styles/Style'
import { isWeb } from '../../hooks/useLayoutType'
const SHEET_ID = "PlaybackPicker";
const SHEET_ID = 'PlaybackPicker'
const PlaybackPickerModal = () => {
const { userProjects = [], createNewProject } = useUserData();
const [isCreating, setIsCreating] = useState(false);
const [webVisible, setWebVisible] = useState(true);
const modalRef = useRef(null);
const { userProjects = [], createNewProject } = useUserData()
const [isCreating, setIsCreating] = useState(false)
const [webVisible, setWebVisible] = useState(true)
const modalRef = useRef(null)
const sanitizedProjects = useMemo(() => {
if (!Array.isArray(userProjects)) {
return [];
return []
}
return userProjects.filter((project) => {
if (!project || project.playbackUrl) {
return false;
return false
}
return !!project?.songUrl;
});
}, [userProjects]);
const hasAnyMusic = Array.isArray(userProjects) && userProjects.length > 0;
return !!project?.songUrl
})
}, [userProjects])
const hasAnyMusic = Array.isArray(userProjects) && userProjects.length > 0
const hideSheet = useCallback(() => {
if (isWeb) {
setWebVisible(false);
setWebVisible(false)
}
try {
const maybePromise = SheetManager.hide(SHEET_ID);
return Promise.resolve(maybePromise);
const maybePromise = SheetManager.hide(SHEET_ID)
return Promise.resolve(maybePromise)
} catch (error) {
console.error("[PlaybackPicker] hide error", error?.message || error);
return Promise.resolve();
console.error('[PlaybackPicker] hide error', error?.message || error)
return Promise.resolve()
}
}, [setWebVisible]);
}, [setWebVisible])
const handleSelectProject = useCallback(
async (project) => {
if (!project?.id) return;
if (!project?.id) return
const hidePromise = hideSheet();
const hidePromise = hideSheet()
await Promise.race([
Promise.resolve(hidePromise),
new Promise((resolve) => setTimeout(resolve, 200)),
]).catch(() => {});
]).catch(() => {})
navigate(Routes.Playback, { project });
navigate(Routes.Playback, { project })
},
[hideSheet]
);
)
const handleCreateNew = useCallback(async () => {
if (isCreating) {
return;
return
}
setIsCreating(true);
setIsCreating(true)
try {
let projectId = null;
if (typeof createNewProject === "function") {
projectId = await createNewProject({ hasLyrics: false });
let projectId = null
if (typeof createNewProject === 'function') {
projectId = await createNewProject({ hasLyrics: false })
}
const hidePromise = hideSheet();
const hidePromise = hideSheet()
await Promise.race([
Promise.resolve(hidePromise),
new Promise((resolve) => setTimeout(resolve, 200)),
]);
])
if (projectId) {
navigate(Routes.WritingLyrics, { projectId });
navigate(Routes.WritingLyrics, { projectId })
} else {
navigate(Routes.WritingLyrics);
navigate(Routes.WritingLyrics)
}
} catch (error) {
console.error("[PlaybackPicker] create project failed", error);
console.error('[PlaybackPicker] create project failed', error)
} finally {
setIsCreating(false);
setIsCreating(false)
}
}, [createNewProject, hideSheet, isCreating]);
}, [createNewProject, hideSheet, isCreating])
const hasProjects = sanitizedProjects.length > 0;
const hasProjects = sanitizedProjects.length > 0
useEffect(() => {
if (!isWeb || !webVisible || typeof document === "undefined") {
return;
if (!isWeb || !webVisible || typeof document === 'undefined') {
return
}
const handlePointerDown = (event) => {
const node = modalRef.current;
const node = modalRef.current
if (!node) {
return;
return
}
const domNode = findNodeHandle(node);
if (domNode && typeof domNode.contains === "function") {
const domNode = findNodeHandle(node)
if (domNode && typeof domNode.contains === 'function') {
if (domNode.contains(event.target)) {
return;
return
}
}
if (domNode === event.target) {
return;
return
}
hideSheet();
};
hideSheet()
}
document.addEventListener("pointerdown", handlePointerDown);
document.addEventListener('pointerdown', handlePointerDown)
return () => {
document.removeEventListener("pointerdown", handlePointerDown);
};
}, [hideSheet, webVisible]);
document.removeEventListener('pointerdown', handlePointerDown)
}
}, [hideSheet, webVisible])
if (isWeb && !webVisible) {
return null;
return null
}
return (
@@ -150,8 +136,7 @@ const PlaybackPickerModal = () => {
<View ref={modalRef} style={{ gap: 18 }} collapsable={false}>
<Text style={styles.title}>Ajouter un playback</Text>
<Text style={styles.description}>
Choisis une musique déjà créée pour y ajouter un playback ou lance un
nouveau projet.
Choisis une musique déjà créée pour y ajouter un playback ou lance un nouveau projet.
</Text>
{hasProjects ? (
@@ -161,7 +146,7 @@ const PlaybackPickerModal = () => {
showsVerticalScrollIndicator={false}
>
{sanitizedProjects.map((project) => {
const coverUri = project?.coverUrl || null;
const coverUri = project?.coverUrl || null
return (
<Pressable
key={project.id}
@@ -175,17 +160,15 @@ const PlaybackPickerModal = () => {
/>
<View style={{ flex: 1, gap: 4 }}>
<Text numberOfLines={1} style={styles.projectTitle}>
{project?.title || "Sans titre"}
{project?.title || 'Sans titre'}
</Text>
<Text numberOfLines={1} style={styles.projectSubtitle}>
{project?.userName || "Moi"}
</Text>
<Text style={styles.projectHint}>
Playback à créer
{project?.userName || 'Moi'}
</Text>
<Text style={styles.projectHint}>Playback à créer</Text>
</View>
</Pressable>
);
)
})}
</ScrollView>
) : (
@@ -198,7 +181,7 @@ const PlaybackPickerModal = () => {
<Text style={styles.emptyDescription}>
{hasAnyMusic
? "Complète d'abord la création de ta chanson, puis reviens ici pour enregistrer le playback."
: "Crée un nouveau projet pour composer ta musique et lancer ton playback."}
: 'Crée un nouveau projet pour composer ta musique et lancer ton playback.'}
</Text>
</View>
)}
@@ -206,29 +189,29 @@ const PlaybackPickerModal = () => {
<GradientButton
title="Créer un nouveau projet"
onPress={handleCreateNew}
containerStyle={{ alignSelf: "center", minWidth: 220 }}
containerStyle={{ alignSelf: 'center', minWidth: 220 }}
disabled={isCreating}
/>
</View>
</AppActionSheet>
);
};
)
}
export default PlaybackPickerModal;
export default PlaybackPickerModal
const styles = StyleSheet.create({
title: {
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
textAlign: 'center',
},
description: {
fontSize: 14,
color: Palette.white,
opacity: 0.85,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
lineHeight: 20,
},
projectCard: {
@@ -260,7 +243,7 @@ const styles = StyleSheet.create({
fontFamily: FONT_FAMILY.InterRegular,
},
emptyState: {
alignItems: "center",
alignItems: 'center',
gap: 8,
paddingVertical: 40,
paddingHorizontal: 20,
@@ -271,14 +254,14 @@ const styles = StyleSheet.create({
fontSize: 18,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
textAlign: 'center',
},
emptyDescription: {
fontSize: 14,
color: Palette.white,
opacity: 0.8,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
lineHeight: 20,
},
});
})
+81 -95
View File
@@ -1,54 +1,44 @@
import React, { useMemo, useRef, useState } from "react";
import {
Dimensions,
FlatList,
Image,
Pressable,
Text,
TextInput,
View,
} from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import useMinuit from "react-native-minuit/src/hooks/useMinuit";
import SwiperFlatList from "react-native-swiper-flatlist";
import { icons } from "../../assets";
import { arrayUnion, playlistsRef } from "../../config/firebase";
import { useUserData } from "../../providers/UserDataProvider";
import { createPlaylist } from "../../screens/Library/Playlists/playlist";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import Style, { size } from "../../styles/Style";
import AppActionSheet from "../AppActionSheet";
import AppCheckbox from "../AppCheckbox";
import GradientButton from "../GradientButton";
import React, { useMemo, useRef, useState } from 'react'
import { Dimensions, FlatList, Image, Pressable, Text, TextInput, View } from 'react-native'
import { SheetManager } from 'react-native-actions-sheet'
import useMinuit from 'react-native-minuit/src/hooks/useMinuit'
import SwiperFlatList from 'react-native-swiper-flatlist'
import { icons } from '../../assets'
import { arrayUnion, playlistsRef } from '../../config/firebase'
import { useUserData } from '../../providers/UserDataProvider'
import { createPlaylist } from '../../screens/Library/Playlists/playlist'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import Style, { size } from '../../styles/Style'
import AppActionSheet from '../AppActionSheet'
import AppCheckbox from '../AppCheckbox'
import GradientButton from '../GradientButton'
const { width } = Dimensions.get("window");
const { width } = Dimensions.get('window')
const PlaylistModal = (props) => {
const scrollRef = useRef(null);
const [selectedId, setSelectedId] = useState("");
const [playlist, setPlaylist] = useState("");
const [isCreating, setIsCreating] = useState(false);
const { currentUID, userPlaylists = [] } = useUserData();
const startAtCreate = props?.payload?.startAtCreate || false;
const { setTooltip } = useMinuit();
const scrollRef = useRef(null)
const [selectedId, setSelectedId] = useState('')
const [playlist, setPlaylist] = useState('')
const [isCreating, setIsCreating] = useState(false)
const { currentUID, userPlaylists = [] } = useUserData()
const startAtCreate = props?.payload?.startAtCreate || false
const { setTooltip } = useMinuit()
const sanitizedPlaylists = useMemo(
() => (Array.isArray(userPlaylists) ? userPlaylists : []),
[userPlaylists],
);
[userPlaylists]
)
const isDuplicateName = useMemo(() => {
const lower = (playlist || "").trim().toLowerCase();
if (!lower) return false;
return sanitizedPlaylists.some(
(p) => (p?.name || "").trim().toLowerCase() === lower,
);
}, [playlist, sanitizedPlaylists]);
const lower = (playlist || '').trim().toLowerCase()
if (!lower) return false
return sanitizedPlaylists.some((p) => (p?.name || '').trim().toLowerCase() === lower)
}, [playlist, sanitizedPlaylists])
const hideSheet = () => {
SheetManager.hide("Playlist");
};
SheetManager.hide('Playlist')
}
return (
<AppActionSheet id="Playlist">
@@ -65,7 +55,7 @@ const PlaylistModal = (props) => {
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
textAlign: 'center',
}}
>
Ajouter ce titre à une playlist
@@ -102,7 +92,7 @@ const PlaylistModal = (props) => {
}
renderItem={({ item }) => (
<AppCheckbox
label={item?.name || "Sans nom"}
label={item?.name || 'Sans nom'}
onPress={() => setSelectedId(item?.id)}
selected={selectedId === item?.id}
/>
@@ -112,37 +102,35 @@ const PlaylistModal = (props) => {
<GradientButton
title="Valider"
containerStyle={{
width: "80%",
alignSelf: "center",
width: '80%',
alignSelf: 'center',
opacity: selectedId ? 1 : 0.5,
}}
disabled={!selectedId}
onPress={async () => {
try {
const projectId = props?.payload?.projectId;
if (!selectedId) return;
const projectId = props?.payload?.projectId
if (!selectedId) return
if (projectId) {
await playlistsRef.doc(selectedId).update({
musics: arrayUnion(projectId),
updatedAt: new Date(),
});
const addedPlaylist = sanitizedPlaylists.find(
(p) => p?.id === selectedId,
);
const name = addedPlaylist?.name || "la playlist";
})
const addedPlaylist = sanitizedPlaylists.find((p) => p?.id === selectedId)
const name = addedPlaylist?.name || 'la playlist'
setTooltip({
type: "success",
type: 'success',
text: `Ajouté à “${name}`,
});
})
}
} catch (e) {
console.log("Add to playlist error", e?.message);
console.log('Add to playlist error', e?.message)
setTooltip({
type: "error",
text: e?.message || "Ajout impossible",
});
type: 'error',
text: e?.message || 'Ajout impossible',
})
} finally {
hideSheet();
hideSheet()
}
}}
/>
@@ -157,8 +145,8 @@ const PlaylistModal = (props) => {
scrollRef.current?.scrollToIndex({
index: 0,
animated: true,
});
setPlaylist("");
})
setPlaylist('')
}}
>
<Text
@@ -179,7 +167,7 @@ const PlaylistModal = (props) => {
fontSize: 20,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueMedium,
textAlign: "center",
textAlign: 'center',
}}
>
Nommer la playlist
@@ -193,8 +181,8 @@ const PlaylistModal = (props) => {
fontSize: 20,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
minWidth: "30%",
maxWidth: "80%",
minWidth: '30%',
maxWidth: '80%',
}}
value={playlist}
onChangeText={setPlaylist}
@@ -206,7 +194,7 @@ const PlaylistModal = (props) => {
fontSize: 12,
color: Palette.red,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
}}
>
Ce nom de playlist existe déjà
@@ -215,61 +203,59 @@ const PlaylistModal = (props) => {
</View>
<GradientButton
title={isCreating ? "Création..." : "Créer la playlist"}
title={isCreating ? 'Création...' : 'Créer la playlist'}
containerStyle={{
width: "80%",
alignSelf: "center",
width: '80%',
alignSelf: 'center',
opacity: playlist?.trim()?.length ? 1 : 0.5,
}}
disabled={!playlist?.trim()?.length || isCreating}
onPress={async () => {
if (!playlist?.trim()?.length || !currentUID) return;
if (!playlist?.trim()?.length || !currentUID) return
try {
setIsCreating(true);
const name = playlist.trim();
const lower = name.toLowerCase();
setIsCreating(true)
const name = playlist.trim()
const lower = name.toLowerCase()
const exists = sanitizedPlaylists.some(
(p) => (p?.name || "").trim().toLowerCase() === lower,
);
(p) => (p?.name || '').trim().toLowerCase() === lower
)
if (exists) {
setTooltip({
type: "error",
text: "Ce nom de playlist existe déjà",
});
return;
type: 'error',
text: 'Ce nom de playlist existe déjà',
})
return
}
await createPlaylist({
createdBy: currentUID,
name,
musics: props?.payload?.projectId
? [props.payload.projectId]
: [],
musics: props?.payload?.projectId ? [props.payload.projectId] : [],
createdAt: new Date(),
updatedAt: new Date(),
});
})
setTooltip({
type: "success",
type: 'success',
text: props?.payload?.projectId
? `Playlist “${name}” créée et musique ajoutée`
: `Playlist “${name}” créée`,
});
})
setPlaylist("");
setPlaylist('')
scrollRef.current?.scrollToIndex({
index: 0,
animated: true,
});
hideSheet();
})
hideSheet()
} catch (e) {
console.log(e);
console.log(e)
setTooltip({
type: "error",
text: e?.message || "Création impossible",
});
type: 'error',
text: e?.message || 'Création impossible',
})
} finally {
setIsCreating(false);
setIsCreating(false)
}
}}
/>
@@ -277,7 +263,7 @@ const PlaylistModal = (props) => {
</View>
</SwiperFlatList>
</AppActionSheet>
);
};
)
}
export default PlaylistModal;
export default PlaylistModal

Some files were not shown because too many files have changed in this diff Show More