feat: fixes and formatter
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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'),
|
||||
}
|
||||
|
||||
@@ -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
@@ -1 +1 @@
|
||||
export * from './constants';
|
||||
export * from './constants'
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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 = "J’ai déjà mes paroles",
|
||||
title = 'J’ai 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
|
||||
|
||||
@@ -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 = "J’ai déjà mes paroles",
|
||||
title = 'J’ai 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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import BottomSheet from "@gorhom/bottom-sheet";
|
||||
import BottomSheet from '@gorhom/bottom-sheet'
|
||||
|
||||
export default BottomSheet;
|
||||
export default BottomSheet
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: "Let’s write lyrics together !",
|
||||
key: 'songwriter',
|
||||
title: 'Céline',
|
||||
description: 'Let’s 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: "Let’s write lyrics together !",
|
||||
key: 'songwriter',
|
||||
title: 'Céline',
|
||||
description: 'Let’s 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)',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' }],
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 l’expérience",
|
||||
}) {
|
||||
export default function ShareBtn({ style, onPress = null, label = 'Partager l’expé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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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' // l’input est dans le contenu → mode interactif OK
|
||||
keyboardBlurBehavior='restore'
|
||||
keyboardBehavior="interactive" // l’input 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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user