update
@@ -0,0 +1,18 @@
|
||||
import moment from 'moment';
|
||||
|
||||
export const validateDate = (date = null) => {
|
||||
if (!date) {
|
||||
return null;
|
||||
}
|
||||
if (moment(date).isValid()) {
|
||||
return date;
|
||||
}
|
||||
if (typeof date?.toDate === 'function') {
|
||||
return date?.toDate();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export function getAge(birthDate) {
|
||||
return moment().diff(birthDate.toDate(), 'years', false);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
responsiveHeight as _responsiveHeight,
|
||||
responsiveWidth as _responsiveWidth,
|
||||
responsiveFontSize as _responsiveFontSize,
|
||||
} from "react-native-responsive-dimensions";
|
||||
|
||||
import {
|
||||
isWeb,
|
||||
isDesktop,
|
||||
isLargeDesktop,
|
||||
isSmallDesktop,
|
||||
isSuperSmallDesktop,
|
||||
} from "../hooks/useLayoutType";
|
||||
|
||||
export const reductionCoeff = (bypass) =>
|
||||
bypass ? 1 : isLargeDesktop ? 3 : isSmallDesktop ? 2.2 : isDesktop ? 2.8 : 1;
|
||||
|
||||
export const responsiveHeight = (height, bypass = false) => {
|
||||
return _responsiveHeight(height) / reductionCoeff(bypass);
|
||||
};
|
||||
|
||||
export const responsiveWidth = (width, bypass = false) => {
|
||||
return _responsiveWidth(width) / reductionCoeff(bypass);
|
||||
};
|
||||
|
||||
export const responsiveFontSize = (fontSize, bypass = false) => {
|
||||
if (isWeb) {
|
||||
if (isSuperSmallDesktop) {
|
||||
return fontSize * 6;
|
||||
} else if (isSmallDesktop) {
|
||||
return fontSize * 7;
|
||||
} else {
|
||||
return fontSize * 7.5;
|
||||
}
|
||||
} else {
|
||||
return _responsiveFontSize(fontSize) / reductionCoeff(bypass);
|
||||
}
|
||||
};
|
||||
|
||||
// test
|
||||
@@ -0,0 +1,74 @@
|
||||
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);
|
||||
}
|
||||
|
||||
export function checkIfPasswordIsStrongEnough({ password }) {
|
||||
const reg =
|
||||
/^(((?=.*[a-z])(?=.*[A-Z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9])))(?=.{6,})/;
|
||||
return reg.test(password);
|
||||
}
|
||||
|
||||
export const formatPhoneNumber = ({ phoneNumber = null }) => {
|
||||
if (!phoneNumber) {
|
||||
return null;
|
||||
}
|
||||
let newPhoneNumber = phoneNumber;
|
||||
|
||||
newPhoneNumber = newPhoneNumber
|
||||
.trim()
|
||||
.replace(/\s+/g, "") // remove spaces
|
||||
.replace(/\D/g, ""); // remove non digits
|
||||
|
||||
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}`;
|
||||
} else {
|
||||
newPhoneNumber = null;
|
||||
}
|
||||
return newPhoneNumber;
|
||||
};
|
||||
|
||||
export function handleFirebaseError(code) {
|
||||
switch (code) {
|
||||
case "auth/user-not-found":
|
||||
return "Ce compte n'existe pas.";
|
||||
case "auth/invalid-verification-code":
|
||||
return "Votre code de validation est incorrect.";
|
||||
case "auth/provider-already-linked":
|
||||
return "Ce compte est déjà lié à un utilisateur.";
|
||||
case "auth/invalid-credential":
|
||||
return "Identifiants incorrects.";
|
||||
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 provider n'est pas activé.";
|
||||
case "auth/invalid-email":
|
||||
return "Email invalide.";
|
||||
case "auth/wrong-password":
|
||||
return "Mot de passe incorrect.";
|
||||
case "auth/invalid-verification-id":
|
||||
return "Impossible de vous authentifié, réessayez dans quelques secondes.";
|
||||
case "auth/invalid-phone-number":
|
||||
return "Numéro de téléphone incorrect.";
|
||||
case "auth/too-many-requests":
|
||||
return "Nous avons détecté une activité inhabituelle et avons bloqué momentanément votre requête, réessayez dans quelques minutes.";
|
||||
case "auth/email-already-in-use":
|
||||
return "Il y a déjà un compte avec cette adresse mail.";
|
||||
case "auth/missing-password":
|
||||
return "Veuillez entrer un mot de passe.";
|
||||
default:
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 162 B |
|
After Width: | Height: | Size: 210 B |
|
After Width: | Height: | Size: 374 B |
|
After Width: | Height: | Size: 249 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 452 B |
|
After Width: | Height: | Size: 232 B |
|
After Width: | Height: | Size: 568 B |
|
After Width: | Height: | Size: 194 B |
|
After Width: | Height: | Size: 505 B |
|
After Width: | Height: | Size: 286 B |
|
After Width: | Height: | Size: 437 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 683 B |
|
After Width: | Height: | Size: 487 B |
|
After Width: | Height: | Size: 572 B |
|
After Width: | Height: | Size: 454 B |
|
After Width: | Height: | Size: 476 B |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 724 B |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 325 B |
|
After Width: | Height: | Size: 783 B |
|
After Width: | Height: | Size: 361 B |
|
After Width: | Height: | Size: 592 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 535 KiB |
|
After Width: | Height: | Size: 594 B |
|
After Width: | Height: | Size: 256 B |
|
After Width: | Height: | Size: 431 B |
|
After Width: | Height: | Size: 320 B |
|
After Width: | Height: | Size: 197 B |
|
After Width: | Height: | Size: 438 B |
|
After Width: | Height: | Size: 139 B |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 152 B |
|
After Width: | Height: | Size: 365 B |
|
After Width: | Height: | Size: 404 B |
|
After Width: | Height: | Size: 513 B |
|
After Width: | Height: | Size: 266 B |
|
After Width: | Height: | Size: 483 B |
|
After Width: | Height: | Size: 508 B |
|
After Width: | Height: | Size: 188 B |
|
After Width: | Height: | Size: 528 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 864 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 404 B |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 419 B |
|
After Width: | Height: | Size: 330 B |
@@ -0,0 +1,185 @@
|
||||
import home from "./UI/tabs/home.png";
|
||||
import tasks from "./UI/tabs/tasks.png";
|
||||
import chat from "./UI/tabs/chat.png";
|
||||
import design from "./UI/tabs/design.png";
|
||||
import quotes from "./UI/tabs/quotes.png";
|
||||
import settings from "./UI/tabs/settings.png";
|
||||
|
||||
export const tabs = {
|
||||
home,
|
||||
tasks,
|
||||
settings,
|
||||
quotes,
|
||||
chat,
|
||||
design,
|
||||
};
|
||||
|
||||
import bell from "./UI/bell.png";
|
||||
import sort from "./UI/sort.png";
|
||||
|
||||
import chevronDown from "./UI/chevronDown.png";
|
||||
import arrowRight from "./UI/arrowRight.png";
|
||||
import threeDots from "./UI/threeDots.png";
|
||||
|
||||
import thumbUp from "./UI/thumbUp.png";
|
||||
import thumbDown from "./UI/thumbDown.png";
|
||||
|
||||
import add from "./UI/add.png";
|
||||
import search from "./UI/search.png";
|
||||
import addFile from "./UI/addFile.png";
|
||||
import send from "./UI/send.png";
|
||||
import eye from "./UI/eye.png";
|
||||
import edit from "./UI/edit.png";
|
||||
import trash from "./UI/trash.png";
|
||||
import message from "./UI/message.png";
|
||||
import check from "./UI/check.png";
|
||||
import checkCircle from "./UI/checkCircle.png";
|
||||
import undo from "./UI/undo.png";
|
||||
import shoppingBag from "./UI/shoppingBag.png";
|
||||
import lock from "./UI/lock.png";
|
||||
import deliveryTime from "./UI/deliveryTime.png";
|
||||
import tools from "./UI/tools.png";
|
||||
|
||||
import picture from "./UI/picture.png";
|
||||
import screenshot from "./UI/screenshot.png";
|
||||
import focus from "./UI/focus.png";
|
||||
|
||||
import android from "./UI/android.png";
|
||||
import ios from "./UI/ios.png";
|
||||
import web from "./UI/web.png";
|
||||
|
||||
import user from "./UI/user.png";
|
||||
import law from "./UI/law.png";
|
||||
import support from "./UI/support.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 figma from "./icons/figma.png";
|
||||
import gitlab from "./icons/gitlab.png";
|
||||
import algolia from "./icons/algolia.png";
|
||||
import play from "./icons/play.png";
|
||||
import disk from "./icons/disk.png";
|
||||
import stars from "./icons/stars.png";
|
||||
import musicLandLogo from "./icons/musicLandLogo.png";
|
||||
|
||||
export const icons = {
|
||||
bell,
|
||||
sort,
|
||||
|
||||
chevronDown,
|
||||
arrowRight,
|
||||
threeDots,
|
||||
|
||||
thumbUp,
|
||||
thumbDown,
|
||||
|
||||
add,
|
||||
search,
|
||||
addFile,
|
||||
send,
|
||||
eye,
|
||||
edit,
|
||||
trash,
|
||||
message,
|
||||
check,
|
||||
checkCircle,
|
||||
undo,
|
||||
shoppingBag,
|
||||
lock,
|
||||
deliveryTime,
|
||||
tools,
|
||||
|
||||
screenshot,
|
||||
picture,
|
||||
attachment,
|
||||
focus,
|
||||
|
||||
android,
|
||||
ios,
|
||||
web,
|
||||
|
||||
cloud,
|
||||
dashboard,
|
||||
file,
|
||||
|
||||
figma,
|
||||
gitlab,
|
||||
algolia,
|
||||
|
||||
user,
|
||||
law,
|
||||
support,
|
||||
play,
|
||||
disk,
|
||||
stars,
|
||||
musicLandLogo,
|
||||
};
|
||||
|
||||
import triangle from "./art/triangle.png";
|
||||
import circle from "./art/circle.png";
|
||||
import square from "./art/square.png";
|
||||
import gradientTriangle from "./art/gradientTriangle.png";
|
||||
|
||||
import coin from "./art/coin.png";
|
||||
import threeCoins from "./art/threeCoins.png";
|
||||
import boost from "./art/boost.png";
|
||||
import boostBig from "./art/boostBig.png";
|
||||
|
||||
export const art = {
|
||||
triangle,
|
||||
circle,
|
||||
square,
|
||||
gradientTriangle,
|
||||
coin,
|
||||
threeCoins,
|
||||
boost,
|
||||
boostBig,
|
||||
};
|
||||
|
||||
import welcome from "./tutorial/welcome.png";
|
||||
import tutorialChat from "./tutorial/chat.png";
|
||||
import coins from "./tutorial/coins.png";
|
||||
import followProgress from "./tutorial/followProgress.png";
|
||||
import tutorialTasks from "./tutorial/tasks.png";
|
||||
|
||||
export const tutorial = {
|
||||
welcome,
|
||||
chat: tutorialChat,
|
||||
coins,
|
||||
followProgress,
|
||||
tasks: tutorialTasks,
|
||||
};
|
||||
|
||||
import loginAppDashboard from "./mockups/loginAppDashboard.png";
|
||||
|
||||
export const mockups = {
|
||||
loginAppDashboard,
|
||||
};
|
||||
|
||||
import writingBG from "./UI/writingBG.png";
|
||||
import studioBG from "./UI/studioBG.png";
|
||||
import studioBG2 from "./UI/studioBG2.png";
|
||||
|
||||
export const background = {
|
||||
writingBG,
|
||||
studioBG,
|
||||
studioBG2,
|
||||
};
|
||||
|
||||
import nathalie from "./UI/nathalie.png";
|
||||
import theo from "./UI/theo.png";
|
||||
|
||||
export const ai = {
|
||||
nathalie,
|
||||
theo,
|
||||
};
|
||||
|
||||
import placeholder from "./UI/placeholder.jpg";
|
||||
|
||||
export const img = {
|
||||
placeholder,
|
||||
};
|
||||
|
After Width: | Height: | Size: 795 KiB |
|
After Width: | Height: | Size: 446 KiB |
|
After Width: | Height: | Size: 905 KiB |
|
After Width: | Height: | Size: 418 KiB |
|
After Width: | Height: | Size: 301 KiB |
|
After Width: | Height: | Size: 438 KiB |
@@ -0,0 +1,9 @@
|
||||
import {Dimensions} from 'react-native';
|
||||
import {resWidth} from '../styles';
|
||||
|
||||
const {width: DIMENSION_WIDTH, height: DIMENSION_HEIGHT} =
|
||||
Dimensions.get('screen');
|
||||
|
||||
const BOTTOM_BAR_HEIGHT = resWidth(80);
|
||||
|
||||
export {DIMENSION_WIDTH, DIMENSION_HEIGHT, BOTTOM_BAR_HEIGHT};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './constants';
|
||||
@@ -0,0 +1,110 @@
|
||||
import React, { useState } from "react";
|
||||
import { Alert, Platform, View, Text } from "react-native";
|
||||
import { PortalProvider } from "@gorhom/portal";
|
||||
|
||||
import { Fonts, Style, gutters } from "../styles";
|
||||
|
||||
import Overlay from "./Overlay";
|
||||
import Button from "./Button";
|
||||
|
||||
const WebAlertModal = ({ title, description, options }) => {
|
||||
const [visible, setVisible] = useState(true);
|
||||
|
||||
const confirmOption = options?.find(({ style }) => style !== "cancel");
|
||||
const cancelOption = options?.find(({ style }) => style === "cancel");
|
||||
|
||||
const onConfirm = () => {
|
||||
setVisible(false);
|
||||
confirmOption?.onPress();
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
setVisible(false);
|
||||
cancelOption?.onPress();
|
||||
};
|
||||
|
||||
return (
|
||||
<PortalProvider>
|
||||
<Overlay isVisible={visible}>
|
||||
<View
|
||||
style={{
|
||||
...Style.containerModal,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
...Fonts({
|
||||
type: "mainTitle",
|
||||
style: {
|
||||
textAlign: "center",
|
||||
marginBottom: gutters / 2,
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
...Fonts({
|
||||
type: "default",
|
||||
style: {
|
||||
textAlign: "center",
|
||||
marginBottom: gutters / 2,
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
text={confirmOption?.text || "OK"}
|
||||
onPress={onConfirm}
|
||||
alternateAction={
|
||||
cancelOption
|
||||
? {
|
||||
text: cancelOption.text,
|
||||
onPress: () => onCancel(),
|
||||
}
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</Overlay>
|
||||
</PortalProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const alertPolyfill = (title, description, options, extra) => {
|
||||
const rootDiv = document.createElement("div");
|
||||
document.body.appendChild(rootDiv);
|
||||
|
||||
const closeModal = () => {
|
||||
document.body.removeChild(rootDiv);
|
||||
};
|
||||
|
||||
const WebAlertComponent = () => (
|
||||
<WebAlertModal
|
||||
title={title}
|
||||
description={description}
|
||||
options={options}
|
||||
onDismiss={closeModal}
|
||||
/>
|
||||
);
|
||||
|
||||
// Render the React component into the div
|
||||
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",
|
||||
});
|
||||
};
|
||||
|
||||
const alert = Platform.OS === "web" ? alertPolyfill : customAlert;
|
||||
|
||||
export default alert;
|
||||
@@ -0,0 +1,74 @@
|
||||
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 = "",
|
||||
url = null,
|
||||
size = responsiveWidth(7),
|
||||
containerStyle = {},
|
||||
forceRawImage = false,
|
||||
badge = {},
|
||||
}) => {
|
||||
const uniqColorById = (uniqId = "test") => {
|
||||
// Calculer un nombre unique à partir de l'ID de l'employé
|
||||
let uniqueNumber = 0;
|
||||
|
||||
for (let i = 0; i < uniqId?.length; i++) {
|
||||
uniqueNumber += uniqId.charCodeAt(i);
|
||||
}
|
||||
|
||||
// génère des couleurs pastels claires
|
||||
const color = `hsl(${uniqueNumber % 360}, 100%, 90%)`;
|
||||
|
||||
return color;
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: 500,
|
||||
...containerStyle,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
...Style.containerRound,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: url ? "transparent" : uniqColorById(name),
|
||||
}}
|
||||
>
|
||||
{url ? (
|
||||
<Image
|
||||
cachePolicy={"memory"}
|
||||
source={{
|
||||
uri: forceRawImage ? url : formatImageURL({ url, size: 200 }),
|
||||
}}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: size / 2,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
style={Fonts({
|
||||
type: "section",
|
||||
color: Palette.darkPurple,
|
||||
style: { fontSize: size / 3 },
|
||||
})}
|
||||
>
|
||||
{getInitials(name)}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<Badge {...badge} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Motion } from "@legendapp/motion";
|
||||
|
||||
import { Fonts, Palette, Style } from "../styles";
|
||||
|
||||
const Badge = ({
|
||||
count = 0,
|
||||
size = 20,
|
||||
customContent = null,
|
||||
backgroundColor = null,
|
||||
} = {}) => {
|
||||
if (!count && !customContent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Motion.View
|
||||
animate={{ scale: 1 }}
|
||||
initial={{ scale: 0 }}
|
||||
transition={{ type: "tween", duration: 0.5 }}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -size / 4,
|
||||
right: -size / 4,
|
||||
backgroundColor: backgroundColor || Palette.red,
|
||||
borderRadius: 500,
|
||||
...Style.containerCenter,
|
||||
width: size,
|
||||
height: size,
|
||||
}}
|
||||
>
|
||||
{customContent ? (
|
||||
customContent()
|
||||
) : (
|
||||
<Motion.Text
|
||||
animate={{ scale: 1 }}
|
||||
initial={{ scale: 0 }}
|
||||
transition={{ type: "tween", duration: 0.5 }}
|
||||
style={{
|
||||
...Fonts({ color: Palette.white }),
|
||||
}}
|
||||
>
|
||||
{count}
|
||||
</Motion.Text>
|
||||
)}
|
||||
</Motion.View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Badge;
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from "reactn";
|
||||
import { Image, Pressable, Text, View } from "react-native";
|
||||
import { responsiveWidth } from "../actions/responsiveSizes.js";
|
||||
|
||||
import { icons } from "../assets";
|
||||
import { Fonts, Style, gutters } from "../styles";
|
||||
|
||||
import { Routes } from "../navigation";
|
||||
import { navigate } from "../navigation/NavigationService";
|
||||
|
||||
import useLayoutType, { sidebarWidth } from "../hooks/useLayoutType.js";
|
||||
|
||||
export default ({ title = "", containerStyle = {} }) => {
|
||||
const { isDesktop } = useLayoutType();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
Style.containerSpaceBetween,
|
||||
{ marginBottom: 20, ...containerStyle },
|
||||
]}
|
||||
>
|
||||
<View style={Style.containerRow}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
...Fonts({
|
||||
type: "mainTitle",
|
||||
style: {
|
||||
marginRight: 10,
|
||||
maxWidth: isDesktop
|
||||
? sidebarWidth - 4 * gutters
|
||||
: responsiveWidth(70),
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={{ ...Style.containerRow }}>
|
||||
<Pressable onPress={() => navigate(Routes.Notifications)}>
|
||||
<Image
|
||||
resizeMode="contain"
|
||||
source={icons.bell}
|
||||
style={{
|
||||
...Style.iconDefault,
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
...Style.itemBadge,
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { StyleSheet, View } from "react-native";
|
||||
|
||||
import { GradientBorderView } from "../GradientBorderView";
|
||||
|
||||
const BorderGradient = ({ children, gradientProps, ...props }) => {
|
||||
return (
|
||||
<GradientBorderView
|
||||
gradientProps={{
|
||||
start: { x: 0, y: 0 },
|
||||
end: { x: 1, y: 0 },
|
||||
...gradientProps
|
||||
}}
|
||||
{...props}>
|
||||
|
||||
<View style={styles.innerContainer}>{children}</View>
|
||||
</GradientBorderView>);
|
||||
|
||||
};
|
||||
|
||||
export default BorderGradient;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
innerContainer: {
|
||||
flex: 1,
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import { useState } from "react";
|
||||
|
||||
const BorderGradient = ({ children, gradientProps, ...props }) => {
|
||||
const defaultGradientProps = {
|
||||
start: {
|
||||
x: 0.5,
|
||||
y: 0
|
||||
},
|
||||
end: {
|
||||
x: 0.5,
|
||||
y: 1
|
||||
},
|
||||
locations: [],
|
||||
colors: [],
|
||||
useAngle: false,
|
||||
angle: 0
|
||||
};
|
||||
|
||||
const { locations, end, start, useAngle, angle, onLayout, colors } =
|
||||
gradientProps;
|
||||
|
||||
const {
|
||||
style,
|
||||
borderWidth,
|
||||
borderRadius,
|
||||
borderTopRightRadius,
|
||||
borderTopLeftRadius,
|
||||
borderBottomLeftRadius,
|
||||
borderBottomRightRadius,
|
||||
borderTopWidth,
|
||||
borderLeftWidth,
|
||||
borderRightWidth,
|
||||
borderBottomWidth
|
||||
} = props;
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const getAngle = () => {
|
||||
if (useAngle) {
|
||||
return angle + "deg";
|
||||
}
|
||||
|
||||
// Math.atan2 handles Infinity
|
||||
const _angle =
|
||||
Math.atan2(
|
||||
state.width * (propEnd.y - propStart.y),
|
||||
state.height * (propEnd.x - propStart.x)
|
||||
) +
|
||||
Math.PI / 2;
|
||||
return _angle + "rad";
|
||||
};
|
||||
|
||||
const getColors = () =>
|
||||
colors.
|
||||
map((color, index) => {
|
||||
const location = locations?.[index] ?? defaultGradientProps.locations;
|
||||
let locationStyle = "";
|
||||
if (location) {
|
||||
locationStyle = " " + location * 100 + "%";
|
||||
}
|
||||
return color + locationStyle;
|
||||
}).
|
||||
join(",");
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
position: "relative"
|
||||
}}>
|
||||
|
||||
<View
|
||||
onLayout={measure}
|
||||
style={[
|
||||
style,
|
||||
{
|
||||
borderWidth,
|
||||
borderRadius,
|
||||
borderTopLeftRadius,
|
||||
borderTopRightRadius,
|
||||
borderBottomLeftRadius,
|
||||
borderBottomRightRadius,
|
||||
borderTopWidth,
|
||||
borderLeftWidth,
|
||||
borderRightWidth,
|
||||
borderBottomWidth,
|
||||
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"
|
||||
}]
|
||||
}>
|
||||
</View>
|
||||
<View style={styles.innerContainer}>{children}</View>
|
||||
</View>);
|
||||
|
||||
};
|
||||
|
||||
export default BorderGradient;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
innerContainer: {
|
||||
flex: 1,
|
||||
margin: 5, // <-- Border Width
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
right: 0,
|
||||
left: 0,
|
||||
overflow: "hidden"
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { View, Text, Pressable } from "react-native";
|
||||
import React from "react";
|
||||
import BorderGradient from "./BorderGradient/BorderGradient";
|
||||
import { Palette, Style } from "../styles";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { FONT_FAMILY } from "../styles/Fonts";
|
||||
|
||||
const BorderGradientButton = () => {
|
||||
return (
|
||||
<Pressable>
|
||||
<BorderGradient
|
||||
gradientProps={{
|
||||
colors: ["#F94697", "#7023F7"],
|
||||
}}
|
||||
style={{
|
||||
height: 50,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 14,
|
||||
overflow: "hidden",
|
||||
backgroundColor: "#73737324",
|
||||
}}
|
||||
>
|
||||
<BlurView
|
||||
intensity={30}
|
||||
tint="dark"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
...Style.containerCenter,
|
||||
borderRadius: 14,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 15,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
}}
|
||||
>
|
||||
J’ai déjà mes paroles
|
||||
</Text>
|
||||
</BlurView>
|
||||
</View>
|
||||
</BorderGradient>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default BorderGradientButton;
|
||||
@@ -0,0 +1,3 @@
|
||||
import BottomSheet from "@gorhom/bottom-sheet";
|
||||
|
||||
export default BottomSheet;
|
||||
@@ -0,0 +1,94 @@
|
||||
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";
|
||||
|
||||
export const SheetScrollView = ScrollView;
|
||||
export const SheetBackdrop = View;
|
||||
|
||||
const BottomSheet = React.forwardRef((props, ref) => {
|
||||
const [showSheet, setShowSheet] = useState(false);
|
||||
|
||||
const bottomSheetRef = useRef();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
snapToIndex: () => {
|
||||
setShowSheet(true);
|
||||
},
|
||||
expand: () => {
|
||||
setShowSheet(true);
|
||||
},
|
||||
collapse: () => closeBottomSheet(),
|
||||
close: () => closeBottomSheet(),
|
||||
}));
|
||||
|
||||
const closeBottomSheet = () => {
|
||||
setShowSheet(false);
|
||||
props.onChange(-1);
|
||||
};
|
||||
|
||||
if (!showSheet) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Motion.View
|
||||
initial={{ right: -500, opacity: 0 }}
|
||||
animate={{ right: 0, opacity: 1 }}
|
||||
style={{
|
||||
position: "fixed",
|
||||
right: 0,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
flex: 1,
|
||||
zIndex: 1000000,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={closeBottomSheet}
|
||||
ref={bottomSheetRef}
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0,0,0,0.4)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
width: isDesktop
|
||||
? sidebarWidth * (isLargeDesktop ? 2 : 1.5)
|
||||
: "100%",
|
||||
backgroundColor: Palette.lightPurple,
|
||||
overflow: "scroll",
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</View>
|
||||
</Motion.View>
|
||||
</Portal>
|
||||
);
|
||||
});
|
||||
// TODO une croix pour fermer sur mobile web
|
||||
|
||||
export default BottomSheet;
|
||||
@@ -0,0 +1,87 @@
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import { Keyboard, StyleSheet } from "react-native";
|
||||
import { AnimatePresence, Motion } from "@legendapp/motion";
|
||||
import { useKeyboard } from "@react-native-community/hooks";
|
||||
|
||||
import BottomSheet from "./BottomSheet";
|
||||
|
||||
import { Palette } from "../styles";
|
||||
import useLayoutType from "../hooks/useLayoutType";
|
||||
|
||||
export default ({
|
||||
children,
|
||||
bottomSheetRef,
|
||||
snapPoints = ["25%", "50%"],
|
||||
handleStyle = {},
|
||||
...rest
|
||||
}) => {
|
||||
const [currentSnapPointIndex, setCurrentSnapPointIndex] = useState(0);
|
||||
|
||||
const { keyboardShown = false } = useKeyboard();
|
||||
const { isWeb } = useLayoutType();
|
||||
|
||||
const handleSheetChanges = useCallback((index) => {
|
||||
setCurrentSnapPointIndex(index);
|
||||
|
||||
if (index <= 0) {
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (bottomSheetRef.current && !isWeb && currentSnapPointIndex > 0) {
|
||||
if (keyboardShown) {
|
||||
bottomSheetRef.current.expand();
|
||||
} else {
|
||||
bottomSheetRef.current.snapToIndex(1);
|
||||
}
|
||||
}
|
||||
}, [keyboardShown]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
{currentSnapPointIndex > 0 ? (
|
||||
<Motion.Pressable
|
||||
style={{ ...StyleSheet.absoluteFill }}
|
||||
onPress={() => bottomSheetRef?.current?.close()}
|
||||
>
|
||||
<Motion.View
|
||||
key={"A"}
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: Palette.black,
|
||||
}}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 0.9 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{
|
||||
default: {
|
||||
type: "spring",
|
||||
},
|
||||
opacity: {
|
||||
type: "timing",
|
||||
},
|
||||
}}
|
||||
></Motion.View>
|
||||
</Motion.Pressable>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
|
||||
<BottomSheet
|
||||
ref={bottomSheetRef}
|
||||
snapPoints={snapPoints}
|
||||
handleStyle={{
|
||||
backgroundColor: Palette.darkPurple,
|
||||
...handleStyle,
|
||||
}}
|
||||
handleIndicatorStyle={{ backgroundColor: Palette.primary }}
|
||||
style={{ zIndex: 100 }}
|
||||
onChange={handleSheetChanges}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</BottomSheet>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
import { Motion } from "@legendapp/motion";
|
||||
import { Text, View } from "react-native";
|
||||
import * as Haptics from "expo-haptics";
|
||||
|
||||
import { Fonts, Palette } from "../styles";
|
||||
import Style, { gutters, mainBorderRadius } from "../styles/Style";
|
||||
import useLayoutType, {
|
||||
isDesktop,
|
||||
isMobile,
|
||||
isNative,
|
||||
} from "../hooks/useLayoutType";
|
||||
|
||||
export default ({
|
||||
type = "primary",
|
||||
theme = "default", // "default" | "radioactiv"
|
||||
|
||||
text,
|
||||
onPress = () => console.log("null"),
|
||||
isAbsoluteBottom = false,
|
||||
|
||||
alternateAction = {},
|
||||
|
||||
contentContainerStyle = {},
|
||||
containerStyle = {},
|
||||
|
||||
textStyle = {},
|
||||
|
||||
isMainDesktopPanel = false,
|
||||
}) => {
|
||||
const buttonWidth = alternateAction?.text ? "49%" : "100%";
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: alternateAction?.text ? "space-between" : "center",
|
||||
...(isAbsoluteBottom
|
||||
? {
|
||||
position: "absolute",
|
||||
bottom: gutters * (isMobile ? 2 : 1),
|
||||
alignItems: "flex-end",
|
||||
...(isDesktop && !isMainDesktopPanel
|
||||
? {
|
||||
maxWidth: 600,
|
||||
minWidth: 400,
|
||||
alignSelf: "center",
|
||||
}
|
||||
: {
|
||||
right: gutters,
|
||||
left: gutters,
|
||||
alignSelf: "center",
|
||||
}),
|
||||
}
|
||||
: {
|
||||
width: "100%",
|
||||
}),
|
||||
...contentContainerStyle,
|
||||
}}
|
||||
>
|
||||
{alternateAction?.text && alternateAction?.onPress ? (
|
||||
<BaseButton
|
||||
type={"secondary"}
|
||||
theme={alternateAction?.theme || theme}
|
||||
text={alternateAction.text}
|
||||
onPress={alternateAction.onPress}
|
||||
textStyle={{
|
||||
...(alternateAction?.textStyle || {}),
|
||||
}}
|
||||
containerStyle={{
|
||||
width: buttonWidth,
|
||||
...(alternateAction?.containerStyle || {}),
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<BaseButton
|
||||
type={type}
|
||||
theme={theme}
|
||||
text={text}
|
||||
onPress={onPress}
|
||||
containerStyle={{
|
||||
width: buttonWidth,
|
||||
...containerStyle,
|
||||
}}
|
||||
textStyle={{
|
||||
...textStyle,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export const BaseButton = ({
|
||||
type = "primary",
|
||||
theme = "default",
|
||||
text,
|
||||
onPress = () => console.log("null"),
|
||||
containerStyle = {},
|
||||
textStyle = {},
|
||||
hasShadow = false,
|
||||
}) => {
|
||||
const primaryColor =
|
||||
theme === "radioactiv" ? Palette.radioactivGreen : Palette.primary;
|
||||
const primaryTransparentColor =
|
||||
theme === "radioactiv"
|
||||
? Palette.transparentRadioactivGreen
|
||||
: Palette.transparentPrimary;
|
||||
|
||||
const textColor = type === "secondary" ? primaryColor : Palette.darkPurple;
|
||||
|
||||
return (
|
||||
<Motion.Pressable
|
||||
whileTap={{ scale: 0.8 }}
|
||||
onPress={() => {
|
||||
if (isNative) {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
onPress();
|
||||
}}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 50,
|
||||
marginTop: gutters / 2,
|
||||
...Style.containerRow,
|
||||
...Style.containerCenter,
|
||||
backgroundColor: primaryColor,
|
||||
borderRadius: mainBorderRadius,
|
||||
...(type === "secondary"
|
||||
? {
|
||||
backgroundColor: primaryTransparentColor,
|
||||
}
|
||||
: hasShadow
|
||||
? { ...Style.defaultShadows }
|
||||
: {}),
|
||||
...containerStyle,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
...Fonts({
|
||||
type: "default",
|
||||
color: textColor,
|
||||
}),
|
||||
...textStyle,
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
</Motion.Pressable>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
import { useState, useEffect } from "reactn";
|
||||
import { Pressable, TextInput, View, Image } from "react-native";
|
||||
import { BlurView } from "expo-blur";
|
||||
|
||||
import { Fonts, gutters, Palette } from "../styles";
|
||||
import Style from "../styles/Style";
|
||||
|
||||
import { chatsRef } from "../config/firebase";
|
||||
|
||||
import { icons } from "../assets";
|
||||
|
||||
import { isWeb } from "../hooks/useLayoutType.js";
|
||||
|
||||
import DocumentDropZone from "./DocumentDropZone.js";
|
||||
import FilesPreview from "./FilesPreview.js";
|
||||
|
||||
const ChatInput = ({
|
||||
message,
|
||||
setMessage,
|
||||
onSendMessage,
|
||||
containerStyle = {},
|
||||
placeholder = "",
|
||||
chatID = null,
|
||||
}) => {
|
||||
const [files, setFiles] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
setFiles([]);
|
||||
}, [chatID]);
|
||||
|
||||
const handleMessageObject = () => {
|
||||
if (message.length > 0 || files.length > 0) {
|
||||
onSendMessage({
|
||||
customPayload: {
|
||||
files,
|
||||
},
|
||||
});
|
||||
setMessage("");
|
||||
setFiles([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (e) => {
|
||||
if (e?.nativeEvent?.key?.toLowerCase() === "enter") {
|
||||
handleMessageObject();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
...Style.containerItem,
|
||||
backgroundColor: Palette.transparentDarkPurple,
|
||||
justifyContent: "center",
|
||||
borderWidth: 1,
|
||||
borderColor: Palette.ultraLightWhite,
|
||||
overflow: "hidden",
|
||||
width: "100%",
|
||||
height: "auto",
|
||||
padding: 0,
|
||||
...containerStyle,
|
||||
}}
|
||||
>
|
||||
<BlurView
|
||||
intensity={30}
|
||||
tint="dark"
|
||||
style={{ flex: 1, justifyContent: "center" }}
|
||||
>
|
||||
<FilesPreview
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
containerStyle={{ margin: gutters / 2, marginBottom: 0 }}
|
||||
/>
|
||||
|
||||
<View style={{ ...Style.containerRow, height: 50 }}>
|
||||
<DocumentDropZone
|
||||
documentID={chatID}
|
||||
collectionRef={chatsRef}
|
||||
shouldReturnObject
|
||||
setFiles={setFiles}
|
||||
customElement={() => (
|
||||
<View style={{ width: 50, ...Style.containerCenter }}>
|
||||
<Image
|
||||
resizeMode="contain"
|
||||
source={icons.addFile}
|
||||
style={{
|
||||
...Style.iconSmall,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
numberOfLines={1}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={Palette.white}
|
||||
value={message}
|
||||
onChangeText={setMessage}
|
||||
style={{
|
||||
width: "85%",
|
||||
...Fonts({ type: "default", style: {} }),
|
||||
}}
|
||||
keyboardAppearance="dark"
|
||||
{...(!isWeb
|
||||
? {
|
||||
returnKeyType: "send",
|
||||
onSubmitEditing: onSendMessage,
|
||||
}
|
||||
: {
|
||||
onKeyPress: handleKeyPress,
|
||||
})}
|
||||
/>
|
||||
|
||||
<Pressable
|
||||
onPress={handleMessageObject}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: 50,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
resizeMode="contain"
|
||||
source={icons.send}
|
||||
style={{
|
||||
...Style.iconSmall,
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
</BlurView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatInput;
|
||||
@@ -0,0 +1,294 @@
|
||||
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 firebase, { chatsRef } from "../config/firebase";
|
||||
|
||||
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";
|
||||
|
||||
export default ({
|
||||
chatID = null,
|
||||
|
||||
layout = "default", // default | taskSideBar
|
||||
|
||||
containerStyle = {},
|
||||
messageListContainerStyle = {},
|
||||
}) => {
|
||||
const [currentUID] = useGlobal("currentUID");
|
||||
const [currentProjectData] = useGlobal("currentProjectData");
|
||||
|
||||
const [isTyping] = useState(false);
|
||||
|
||||
const flatListRef = useRef();
|
||||
|
||||
const { isNative } = useLayoutType();
|
||||
const { keyboardShown = false } = useKeyboard();
|
||||
|
||||
const { data: messageList } = useDataFromRef({
|
||||
ref: chatID
|
||||
? chatsRef
|
||||
.doc(chatID)
|
||||
.collection("messages")
|
||||
.orderBy("createdAt", "desc")
|
||||
.limit(50)
|
||||
: null,
|
||||
simpleRef: false,
|
||||
listener: true,
|
||||
condition: chatID,
|
||||
refreshArray: [chatID],
|
||||
documentID: "messageID",
|
||||
});
|
||||
|
||||
let conversation = [
|
||||
isTyping ? { senderID: "minuit.ai", userTyping: true } : null,
|
||||
...(messageList || []),
|
||||
].filter((item) => item);
|
||||
|
||||
if (layout === "default") {
|
||||
conversation = conversation.reverse();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (flatListRef?.current && isNative && keyboardShown) {
|
||||
flatListRef?.current?.scrollToEnd?.({ animated: true });
|
||||
}
|
||||
}, [keyboardShown, isNative]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
height: responsiveHeight(85, true),
|
||||
...containerStyle,
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1, ...messageListContainerStyle }}>
|
||||
<FlatList
|
||||
ref={flatListRef}
|
||||
data={conversation}
|
||||
estimatedItemSize={50}
|
||||
scrollEnabled
|
||||
contentContainerStyle={{
|
||||
paddingBottom: responsiveHeight(40),
|
||||
}}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyExtractor={(item, index) =>
|
||||
item?.messageID
|
||||
? `${item?.messageID?.toString()}-${index}`
|
||||
: `no-messageID-${index}`
|
||||
}
|
||||
renderItem={({
|
||||
item: {
|
||||
createdAt = null,
|
||||
senderID,
|
||||
senderName = "",
|
||||
senderProfilePicture = null,
|
||||
text = "",
|
||||
userTyping = false,
|
||||
files = [],
|
||||
},
|
||||
index,
|
||||
}) => {
|
||||
const isCurrentUser = senderID === currentUID;
|
||||
const isChatbot = senderID === "minuit.ai";
|
||||
|
||||
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];
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDayChange && (
|
||||
<View
|
||||
style={{
|
||||
...Style.containerCenter,
|
||||
marginBottom: gutters / 2,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
...Fonts({
|
||||
type: "default",
|
||||
color: Palette.white,
|
||||
style: { textAlign: "center", opacity: 0.5 },
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{moment(createdAt?.toDate()).format(
|
||||
"[Le] DD/MM/YYYY [à] HH:mm"
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<View
|
||||
style={{
|
||||
...Style.separatorHorizontal,
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View
|
||||
style={[
|
||||
Style.containerRow,
|
||||
{
|
||||
flexDirection: isCurrentUser ? "row-reverse" : "row",
|
||||
alignItems: "flex-end",
|
||||
width: "100%",
|
||||
marginBottom: gutters / 2,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{!isCurrentUser && (
|
||||
<View
|
||||
style={{
|
||||
...Style.containerRound,
|
||||
...Style.containerCenter,
|
||||
...(isCurrentUser
|
||||
? {
|
||||
marginLeft: gutters / 2,
|
||||
backgroundColor: Palette.primary,
|
||||
}
|
||||
: {
|
||||
marginRight: gutters / 2,
|
||||
backgroundColor: "transparent",
|
||||
borderColor: Palette.primary,
|
||||
borderWidth: 1,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
name={
|
||||
isChatbot ? "m" : senderData?.name || senderName || ""
|
||||
}
|
||||
url={
|
||||
senderData?.profilePictureURL ||
|
||||
senderProfilePicture ||
|
||||
null
|
||||
}
|
||||
size={35}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View
|
||||
style={{
|
||||
alignItems: isCurrentUser ? "flex-end" : "flex-start",
|
||||
}}
|
||||
>
|
||||
{files?.map((props, index) => (
|
||||
<RenderChatFile
|
||||
key={index}
|
||||
{...props}
|
||||
containerStyle={{
|
||||
marginBottom:
|
||||
index !== files?.length - 1
|
||||
? gutters / 2
|
||||
: text?.length > 0 || userTyping
|
||||
? gutters / 2
|
||||
: 0,
|
||||
}}
|
||||
/>
|
||||
)) || null}
|
||||
|
||||
{(text?.length > 0 || userTyping) && (
|
||||
<View
|
||||
style={{
|
||||
...bubbleStyle({
|
||||
isCurrentUser,
|
||||
customStyle: {
|
||||
marginBottom: files?.length > 0 ? gutters / 2 : 0,
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{userTyping ? (
|
||||
<TypingLoader />
|
||||
) : (
|
||||
<HyperlinkContainer>
|
||||
<Text
|
||||
style={Fonts({
|
||||
type: "default",
|
||||
style: {
|
||||
color: Palette.white,
|
||||
textAlign: isCurrentUser ? "right" : "left",
|
||||
width: "100%",
|
||||
},
|
||||
})}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
</HyperlinkContainer>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export const onSendMessage = async ({
|
||||
chatID = null,
|
||||
projectID = null,
|
||||
message = "",
|
||||
setMessage = () => {},
|
||||
setIsTyping = () => {},
|
||||
customPayload = {},
|
||||
}) => {
|
||||
try {
|
||||
const currentUID = getGlobal()?.currentUID || null;
|
||||
const { name = "", profilePictureURL = null } =
|
||||
getGlobal()?.currentUserData || {};
|
||||
|
||||
if (message.length > 0 || customPayload?.files?.length > 0) {
|
||||
Keyboard.dismiss();
|
||||
setMessage("");
|
||||
|
||||
const messageData = {
|
||||
projectID,
|
||||
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
senderID: currentUID,
|
||||
senderName: formatNameForConfidentiality({ name }),
|
||||
senderProfilePicture: profilePictureURL || null,
|
||||
text: message,
|
||||
...customPayload,
|
||||
};
|
||||
|
||||
await chatsRef.doc(chatID).collection("messages").add(messageData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsTyping(false);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
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 { 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",
|
||||
style: {
|
||||
textAlign: "center",
|
||||
marginBottom: gutters,
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export const Description = ({ children }) => {
|
||||
return (
|
||||
<Text
|
||||
style={{
|
||||
...Fonts({
|
||||
type: "default",
|
||||
style: {
|
||||
textAlign: "center",
|
||||
marginBottom: gutters / 2,
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export const Button = ({
|
||||
label,
|
||||
onPress,
|
||||
type = "primary",
|
||||
containerStyle = {},
|
||||
}) => {
|
||||
return (
|
||||
<MinuitButton
|
||||
containerStyle={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
...containerStyle,
|
||||
}}
|
||||
text={label}
|
||||
onPress={onPress}
|
||||
type={type}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Input = (props) => {
|
||||
return <MinuitInput {...props} setValue={props.onChangeText} />;
|
||||
};
|
||||
@@ -0,0 +1,405 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useGlobal,
|
||||
} from "reactn";
|
||||
import { Image, Pressable, Text, View } from "react-native";
|
||||
import * as ImagePicker from "expo-image-picker";
|
||||
import moment from "moment";
|
||||
import { useActionSheet } from "@expo/react-native-action-sheet";
|
||||
import * as DocumentPicker from "expo-document-picker";
|
||||
import Compressor from "react-native-compressor";
|
||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit";
|
||||
|
||||
import { arrayUnion } from "../config/firebase";
|
||||
|
||||
import Style, { gutterConstant, mainBorderRadius } from "../styles/Style";
|
||||
import { Fonts, Palette } from "../styles";
|
||||
import { icons } from "../assets";
|
||||
|
||||
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",
|
||||
];
|
||||
|
||||
const DocumentDropZone = ({
|
||||
containerStyle = {},
|
||||
|
||||
documentID = null,
|
||||
documentExists = false,
|
||||
|
||||
collectionRef = null,
|
||||
|
||||
setFiles = null,
|
||||
customElement = null,
|
||||
shouldReturnObject = false,
|
||||
}) => {
|
||||
const dropRef = useRef(null);
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const { setIsLoading, setTooltip } = useMinuit();
|
||||
const { isDesktop, isWeb, isNative } = useLayoutType();
|
||||
const { showActionSheetWithOptions } = useActionSheet();
|
||||
|
||||
useEffect(() => {
|
||||
if (isWeb && dropRef.current) {
|
||||
const el = dropRef.current;
|
||||
|
||||
const handleDragIn = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleDragOut = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!dropRef.current.contains(e.relatedTarget)) {
|
||||
console.log("drag left");
|
||||
setIsDragging(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
|
||||
let files = [...e.dataTransfer.files];
|
||||
|
||||
if (files.length > 0) {
|
||||
for (const file of files) {
|
||||
handleWebFile({ file });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
||||
}, [dropRef?.current]);
|
||||
|
||||
const handleWebFile = ({ file }) => {
|
||||
try {
|
||||
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
|
||||
|
||||
console.log("file", file);
|
||||
|
||||
onUploadDocument({ files: [{ name: file.name, uri, type }] });
|
||||
};
|
||||
|
||||
reader.onerror = (err) => {
|
||||
console.error("FileReader error", err);
|
||||
};
|
||||
|
||||
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" });
|
||||
}
|
||||
};
|
||||
|
||||
const onAddFile = async ({} = {}) => {
|
||||
try {
|
||||
if (isNative) {
|
||||
const cancelButtonIndex = NATIVE_OPTIONS.length - 1;
|
||||
|
||||
showActionSheetWithOptions(
|
||||
{
|
||||
options: NATIVE_OPTIONS,
|
||||
cancelButtonIndex,
|
||||
userInterfaceStyle: "dark",
|
||||
...Style.actionSheet,
|
||||
},
|
||||
async (selectedIndex) => {
|
||||
if (selectedIndex !== cancelButtonIndex) {
|
||||
switch (selectedIndex) {
|
||||
case 0:
|
||||
onChooseLibrary();
|
||||
break;
|
||||
case 1:
|
||||
onChooseDocumentPicker();
|
||||
break;
|
||||
case 2:
|
||||
onTakePicture();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
onChooseDocumentPicker();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
setTooltip({ text: error.message, type: "error" });
|
||||
} finally {
|
||||
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];
|
||||
|
||||
onUploadDocument({
|
||||
files: [
|
||||
{
|
||||
name: getAssetName({ fileName, uri }),
|
||||
uri,
|
||||
type: "IMAGE",
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
throw new Error("Aucune image sélectionnée");
|
||||
}
|
||||
};
|
||||
|
||||
const onChooseDocumentPicker = async ({} = {}) => {
|
||||
const result = await DocumentPicker.getDocumentAsync({
|
||||
type: "*/*",
|
||||
copyToCacheDirectory: false,
|
||||
});
|
||||
|
||||
if (result?.assets?.length > 0) {
|
||||
const { name = "", uri = null } = result?.assets[0] || {};
|
||||
|
||||
if (!uri) {
|
||||
throw new Error("Erreur lors de l'ajout du document");
|
||||
}
|
||||
|
||||
onUploadDocument({
|
||||
files: [
|
||||
{
|
||||
name: name || getDefaultFileName(),
|
||||
uri,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
console.log(result);
|
||||
throw new Error("Erreur lors de l'ajout du document");
|
||||
}
|
||||
};
|
||||
|
||||
const onTakePicture = useCallback(async () => {
|
||||
const { status } = await ImagePicker.requestCameraPermissionsAsync();
|
||||
|
||||
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];
|
||||
|
||||
onUploadDocument({
|
||||
files: [
|
||||
{
|
||||
name: getAssetName({ fileName, uri }),
|
||||
uri,
|
||||
type: "IMAGE",
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
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 getAssetName = ({ fileName = "", uri = "" }) => {
|
||||
let name = fileName;
|
||||
|
||||
if (!name && uri?.startsWith("file://")) {
|
||||
const splittedArray = uri?.split("/") || [];
|
||||
name = splittedArray?.[splittedArray?.length - 1] || "";
|
||||
}
|
||||
|
||||
if (!name?.length) {
|
||||
let extension = uri?.split(";")?.[0]?.split("/")?.[1] || "";
|
||||
|
||||
const defaultFileName = getDefaultFileName();
|
||||
|
||||
if (extension?.length) {
|
||||
name = `${defaultFileName}.${extension}`;
|
||||
} else {
|
||||
name = `${defaultFileName}`;
|
||||
}
|
||||
}
|
||||
|
||||
return name;
|
||||
};
|
||||
|
||||
const onUploadDocument = async ({ files = [] } = {}) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const filesToUpdate = [];
|
||||
|
||||
await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const { name = "", uri = null } = file;
|
||||
|
||||
console.log(file);
|
||||
|
||||
if (!uri) {
|
||||
throw new Error("Erreur lors de l'ajout du document");
|
||||
}
|
||||
|
||||
const fileName = name || getDefaultFileName();
|
||||
let type = "FILE";
|
||||
|
||||
if (name?.toLowerCase()?.match(/\.(jpeg|jpg|gif|png)$/) != null) {
|
||||
type = "IMAGE";
|
||||
}
|
||||
|
||||
if (name?.toLowerCase()?.match(/\.(mp4|mov|avi|mkv)$/) != null) {
|
||||
type = "VIDEO";
|
||||
}
|
||||
|
||||
let compressedURI = uri;
|
||||
let thumbnailURI = null;
|
||||
|
||||
if (isNative) {
|
||||
if (type === "IMAGE") {
|
||||
compressedURI = await Compressor.Image.compress(uri, {
|
||||
compressionMethod: "manual",
|
||||
maxWidth: 1000,
|
||||
quality: 0.8,
|
||||
});
|
||||
} 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) {
|
||||
filesToUpdate.push({
|
||||
name: fileName,
|
||||
uri: resultURI,
|
||||
type,
|
||||
thumbnailURI,
|
||||
});
|
||||
} else {
|
||||
filesToUpdate.push(resultURI);
|
||||
}
|
||||
} else {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
setTooltip({ text: "Document(s) ajouté(s) avec succès" });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
setTooltip({ text: error.message, type: "error" });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!documentID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View ref={dropRef}>
|
||||
<Pressable onPress={onAddFile}>
|
||||
{customElement?.() || (
|
||||
<View
|
||||
style={{
|
||||
...Style.containerCenter,
|
||||
backgroundColor: isDragging
|
||||
? Palette.transparentGreen
|
||||
: Palette.transparentPrimary,
|
||||
borderRadius: mainBorderRadius,
|
||||
borderStyle: "dashed",
|
||||
borderWidth: 2,
|
||||
borderColor: isDragging ? Palette.green : Palette.primary,
|
||||
...containerStyle,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={icons.screenshot}
|
||||
style={{
|
||||
...Style.iconLarge,
|
||||
marginBottom: gutterConstant,
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
...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."}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentDropZone;
|
||||
@@ -0,0 +1,40 @@
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
if (numRetries < 15) {
|
||||
const timer = setTimeout(() => {
|
||||
setLoading(true);
|
||||
}, 2500);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [numRetries]);
|
||||
|
||||
const handleError = () => {
|
||||
setLoading(false);
|
||||
setNumRetries(numRetries + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
{loading && numRetries < 15 && uri ? (
|
||||
<Image
|
||||
source={uri}
|
||||
contentFit="cover"
|
||||
transition={500}
|
||||
{...props}
|
||||
onError={handleError}
|
||||
/>
|
||||
) : (
|
||||
<View {...props}>{children}</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default DynamicImage;
|
||||
@@ -0,0 +1,60 @@
|
||||
import React, { useGlobal } from "reactn";
|
||||
import { View, Text } from "react-native";
|
||||
|
||||
import useLayoutType from "../hooks/useLayoutType";
|
||||
|
||||
import Button from "./Button";
|
||||
|
||||
import { Style, Fonts } from "../styles";
|
||||
import { responsiveScreenHeight } from "react-native-responsive-dimensions";
|
||||
|
||||
const EmptyFlashListPlaceholder = ({
|
||||
loading = false,
|
||||
text = "-",
|
||||
buttonData = {},
|
||||
}) => {
|
||||
const [, setShowSearch] = useGlobal("showSearch");
|
||||
|
||||
const { isMobile } = useLayoutType;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Text style={Fonts({ type: "default", style: { textAlign: "center" } })}>
|
||||
Chargement en cours...
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
...Style.containerCenter,
|
||||
alignSelf: "center",
|
||||
width: isMobile ? "100%" : "50%",
|
||||
height: responsiveScreenHeight(50),
|
||||
}}
|
||||
>
|
||||
<Text style={Fonts({ type: "default", style: { textAlign: "center" } })}>
|
||||
{text}
|
||||
</Text>
|
||||
|
||||
{buttonData?.text?.length > 0 && (
|
||||
<Button
|
||||
text={buttonData?.text || "Ajouter une tâche"}
|
||||
type="secondary"
|
||||
onPress={() => {
|
||||
setShowSearch(false);
|
||||
|
||||
buttonData?.onPress?.() || (() => console.log("null"));
|
||||
}}
|
||||
containerStyle={{
|
||||
alignSelf: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptyFlashListPlaceholder;
|
||||
@@ -0,0 +1,134 @@
|
||||
import { useContext, useGlobal } from "reactn";
|
||||
import { ScrollView, Text, View, Image, Pressable } from "react-native";
|
||||
|
||||
import firebase, { arrayRemove } from "../config/firebase";
|
||||
|
||||
import { getFileNameFromURL } from "../helpers";
|
||||
import { Fonts, Palette, Style, gutters } from "../styles";
|
||||
import { icons } from "../assets";
|
||||
|
||||
import { WebViewContext } from "../providers/WebViewProvider";
|
||||
import alert from "./Alert";
|
||||
|
||||
export default ({
|
||||
files = [],
|
||||
setFiles = null,
|
||||
documentID = null,
|
||||
collectionRef = null,
|
||||
containerStyle = {},
|
||||
}) => {
|
||||
const [, setIsLoading] = useGlobal("_isLoading");
|
||||
const [, setTooltip] = useGlobal("_tooltip");
|
||||
|
||||
const { setWebViewUrl } = useContext(WebViewContext);
|
||||
|
||||
const onDeleteFile = async (url) => {
|
||||
alert(
|
||||
"Êtes-vous sûr ?",
|
||||
"Cette action est irréversible.",
|
||||
[
|
||||
{
|
||||
text: "Annuler",
|
||||
style: "cancel",
|
||||
},
|
||||
{
|
||||
text: "Confirmer",
|
||||
onPress: async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
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 !",
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
style: "confirm",
|
||||
},
|
||||
],
|
||||
{ cancelable: false }
|
||||
);
|
||||
};
|
||||
|
||||
if (files.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<ScrollView
|
||||
horizontal
|
||||
style={{ ...containerStyle }}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
>
|
||||
{files.map((file, index) => {
|
||||
const uri = file.uri || file;
|
||||
|
||||
return (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
Style.containerItem,
|
||||
Style.containerRow,
|
||||
{
|
||||
backgroundColor: Palette.transparentPrimary,
|
||||
paddingVertical: gutters / 4,
|
||||
paddingHorizontal: 20,
|
||||
marginRight: 10,
|
||||
maxWidth: files?.length > 1 ? 250 : 400,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={Fonts({
|
||||
type: "default",
|
||||
color: Palette.white,
|
||||
style: {
|
||||
maxWidth: files?.length > 1 ? 100 : 200,
|
||||
marginRight: 10,
|
||||
},
|
||||
})}
|
||||
>
|
||||
{getFileNameFromURL({ url: uri })}
|
||||
</Text>
|
||||
|
||||
{setFiles && (
|
||||
<Pressable onPress={() => onDeleteFile(uri)}>
|
||||
<Image
|
||||
source={icons.trash}
|
||||
style={[Style.iconDefault, { marginRight: 5 }]}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
<Pressable onPress={() => setWebViewUrl(uri)}>
|
||||
<Image
|
||||
source={icons.eye}
|
||||
style={Style.iconDefault}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
|
||||
import { AnimatableNumericValue, StyleSheet, View } from "react-native";
|
||||
import { LinearGradientProps } from "../../LinearGradient/LinearGradient";
|
||||
import MaskedView from "../../MaskedView/MaskedView";
|
||||
import LinearGradient from "react-native-linear-gradient";
|
||||
|
||||
|
||||
export type GradientProps = Omit<
|
||||
LinearGradientProps,
|
||||
"style" | "pointerEvents">;
|
||||
|
||||
|
||||
export type RequiredGradientBorderProps = {
|
||||
/**
|
||||
* Props to be passed to the gradient component. See `react-native-linear-gradient` for full list. Requires "colors" prop.
|
||||
*/
|
||||
gradientProps: GradientProps;
|
||||
};
|
||||
|
||||
type BorderProps = {
|
||||
/**
|
||||
* Width of border.
|
||||
*/
|
||||
borderWidth?: number;
|
||||
|
||||
/**
|
||||
* Border applied to top of view, overrides borderWidth.
|
||||
*/
|
||||
borderTopWidth?: number;
|
||||
/**
|
||||
* Border applied to left side of view, overrides borderWidth.
|
||||
*/
|
||||
borderLeftWidth?: number;
|
||||
|
||||
/**
|
||||
* Border applied to bottom side of view, overrides borderWidth.
|
||||
*/
|
||||
borderBottomWidth?: number;
|
||||
|
||||
/**
|
||||
* Border appled to right side of view, overrides borderWidth.
|
||||
*/
|
||||
borderRightWidth?: number;
|
||||
/**
|
||||
* Border radius applied to each corner.
|
||||
*/
|
||||
borderRadius?: AnimatableNumericValue | string | undefined;
|
||||
/**
|
||||
* Border radius applied to top right corner, Overrides borderRadius.
|
||||
*/
|
||||
borderTopRightRadius?: AnimatableNumericValue | string | undefined;
|
||||
/**
|
||||
* Border radius applied to top left corner. Overrides borderRadius
|
||||
*/
|
||||
borderTopLeftRadius?: AnimatableNumericValue | string | undefined;
|
||||
/**
|
||||
* Border radius applied to bottom right corner. Overrides borderRadius
|
||||
*/
|
||||
borderBottomRightRadius?: AnimatableNumericValue | string | undefined;
|
||||
/**
|
||||
* Border radius applied to bottom left corner. Overrides borderRadius
|
||||
*/
|
||||
borderBottomLeftRadius?: AnimatableNumericValue | string | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* A component that applies a gradient border to the parent.
|
||||
* Should be placed as the last child of the parent so that it isn't overlapped.
|
||||
* @example
|
||||
* ```tsx
|
||||
* <View>
|
||||
* <GradientBorder
|
||||
* borderWidth={2}
|
||||
* gradientProps={{
|
||||
* colors: ['red', 'blue']
|
||||
* }}
|
||||
* />
|
||||
* </View>
|
||||
* ```
|
||||
*/
|
||||
export default function GradientBorder({
|
||||
gradientProps,
|
||||
borderWidth,
|
||||
borderRadius,
|
||||
borderTopRightRadius,
|
||||
borderTopLeftRadius,
|
||||
borderBottomLeftRadius,
|
||||
borderBottomRightRadius,
|
||||
borderTopWidth,
|
||||
borderLeftWidth,
|
||||
borderRightWidth,
|
||||
borderBottomWidth
|
||||
}: RequiredGradientBorderProps & BorderProps) {
|
||||
return (
|
||||
<MaskedView
|
||||
maskElement={
|
||||
<View
|
||||
pointerEvents="none"
|
||||
style={[
|
||||
{
|
||||
borderWidth,
|
||||
borderRadius,
|
||||
borderTopLeftRadius,
|
||||
borderTopRightRadius,
|
||||
borderBottomLeftRadius,
|
||||
borderBottomRightRadius,
|
||||
borderTopWidth,
|
||||
borderLeftWidth,
|
||||
borderRightWidth,
|
||||
borderBottomWidth
|
||||
},
|
||||
StyleSheet.absoluteFill]
|
||||
}
|
||||
collapsable={false} />
|
||||
|
||||
}
|
||||
style={[StyleSheet.absoluteFill]}
|
||||
pointerEvents="none">
|
||||
|
||||
<LinearGradient
|
||||
style={StyleSheet.absoluteFill}
|
||||
pointerEvents="none"
|
||||
{...gradientProps} />
|
||||
|
||||
</MaskedView>);
|
||||
|
||||
}
|
||||