This commit is contained in:
Philip Cesar Garay
2025-07-31 21:25:33 +08:00
parent 5cfbc81e3a
commit 84fc719a10
307 changed files with 46470 additions and 0 deletions
+18
View File
@@ -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);
}
+40
View File
@@ -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
+74
View File
@@ -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;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

+185
View File
@@ -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,
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 795 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

+9
View File
@@ -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};
+1
View File
@@ -0,0 +1 @@
export * from './constants';
+110
View File
@@ -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;
+74
View File
@@ -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>
);
};
+49
View File
@@ -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;
+61
View File
@@ -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"
}
});
+54
View File
@@ -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,
}}
>
Jai déjà mes paroles
</Text>
</BlurView>
</View>
</BorderGradient>
</Pressable>
);
};
export default BorderGradientButton;
+3
View File
@@ -0,0 +1,3 @@
import BottomSheet from "@gorhom/bottom-sheet";
export default BottomSheet;
+94
View File
@@ -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;
+87
View File
@@ -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>
</>
);
};
+151
View File
@@ -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>
);
};
+141
View File
@@ -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;
+294
View File
@@ -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);
}
};
+8
View File
@@ -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;
+77
View File
@@ -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} />;
};
+405
View File
@@ -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;
+40
View File
@@ -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;
+134
View File
@@ -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>);
}

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