update
This commit is contained in:
@@ -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>);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
View,
|
||||
ViewProps,
|
||||
ViewStyle,
|
||||
} from "react-native";
|
||||
import GradientBorder, { RequiredGradientBorderProps } from "./GradientBorder";
|
||||
import omit from "lodash/omit";
|
||||
|
||||
type GradientBorderViewProps = Omit<
|
||||
ViewStyle,
|
||||
| "paddingLeft"
|
||||
| "paddingRight"
|
||||
| "paddingTop"
|
||||
| "paddingBottom"
|
||||
| "padding"
|
||||
| "borderColor"
|
||||
| "borderLeftColor"
|
||||
| "borderRightColor"
|
||||
| "borderTopColor"
|
||||
| "borderBottomColor"
|
||||
>;
|
||||
|
||||
export type GradientBorderViewStyle = StyleProp<
|
||||
GradientBorderViewProps & {
|
||||
paddingLeft?: number;
|
||||
paddingRight?: number;
|
||||
paddingTop?: number;
|
||||
paddingBottom?: number;
|
||||
padding?: number;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* A view that applies a gradient border. `gradientProps` is required and can be used to control the gradient (react-native-linear-gradient props),
|
||||
* and `borderWidth` is required. See
|
||||
* @example
|
||||
* <GradientBorderView
|
||||
* style={{borderWidth: 50, height: 100, width: 100,}}
|
||||
* gradientProps={{
|
||||
* colors: ['red', 'blue']
|
||||
* }}
|
||||
* />
|
||||
*/
|
||||
export const GradientBorderView = ({
|
||||
gradientProps,
|
||||
...props
|
||||
}: Omit<ViewProps, "style"> & {
|
||||
style?: GradientBorderViewStyle;
|
||||
} & RequiredGradientBorderProps) => {
|
||||
const styles = StyleSheet.flatten(props.style);
|
||||
const userAllPadding = styles.padding ? styles.padding : 0;
|
||||
const compensationAllPadding = styles.borderWidth ? styles.borderWidth : 0;
|
||||
function calcPaddingForSide(
|
||||
paddingName:
|
||||
| "paddingLeft"
|
||||
| "paddingRight"
|
||||
| "paddingTop"
|
||||
| "paddingBottom",
|
||||
borderWidthName:
|
||||
| "borderTopWidth"
|
||||
| "borderBottomWidth"
|
||||
| "borderLeftWidth"
|
||||
| "borderRightWidth"
|
||||
) {
|
||||
const userPadding =
|
||||
typeof styles[paddingName] !== "undefined"
|
||||
? styles[paddingName]!
|
||||
: userAllPadding;
|
||||
const compensationPadding =
|
||||
typeof styles[borderWidthName] !== "undefined"
|
||||
? styles[borderWidthName]!
|
||||
: compensationAllPadding;
|
||||
return compensationPadding + userPadding;
|
||||
}
|
||||
|
||||
if (
|
||||
__DEV__ &&
|
||||
!(
|
||||
styles.borderWidth ||
|
||||
styles.borderTopWidth ||
|
||||
styles.borderLeftWidth ||
|
||||
styles.borderRightWidth ||
|
||||
styles.borderBottomWidth
|
||||
)
|
||||
) {
|
||||
console.warn(
|
||||
"No borderWidth was passed in the GradientBorderView style, no border will be shown."
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
{...props}
|
||||
style={[
|
||||
{
|
||||
paddingLeft: calcPaddingForSide("paddingLeft", "borderLeftWidth"),
|
||||
paddingRight: calcPaddingForSide("paddingRight", "borderRightWidth"),
|
||||
paddingBottom: calcPaddingForSide(
|
||||
"paddingBottom",
|
||||
"borderBottomWidth"
|
||||
),
|
||||
paddingTop: calcPaddingForSide("paddingTop", "borderTopWidth"),
|
||||
},
|
||||
omit(styles, [
|
||||
"borderWidth",
|
||||
"borderTopWidth",
|
||||
"borderLeftWidth",
|
||||
"borderRightWidth",
|
||||
"borderBottomWidth",
|
||||
]),
|
||||
]}
|
||||
>
|
||||
{props.children}
|
||||
<GradientBorder
|
||||
gradientProps={gradientProps}
|
||||
borderRadius={styles.borderRadius}
|
||||
borderWidth={styles.borderWidth}
|
||||
borderBottomWidth={styles.borderBottomWidth}
|
||||
borderRightWidth={styles.borderRightWidth}
|
||||
borderLeftWidth={styles.borderLeftWidth}
|
||||
borderTopWidth={styles.borderTopWidth}
|
||||
borderTopLeftRadius={styles.borderTopLeftRadius}
|
||||
borderTopRightRadius={styles.borderTopRightRadius}
|
||||
borderBottomRightRadius={styles.borderBottomRightRadius}
|
||||
borderBottomLeftRadius={styles.borderBottomLeftRadius}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import {
|
||||
GradientBorderView,
|
||||
GradientBorderViewStyle,
|
||||
} from "./components/GradientBorderView";
|
||||
import { GradientProps } from "./components/GradientBorder";
|
||||
|
||||
export { GradientBorderView };
|
||||
export type { GradientBorderViewStyle, GradientProps as GP };
|
||||
@@ -0,0 +1,40 @@
|
||||
import { View, Text, Pressable } from "react-native";
|
||||
import React from "react";
|
||||
import { LinearGradient } from "./LinearGradient/LinearGradient";
|
||||
import { Palette, Style } from "../styles";
|
||||
import { FONT_FAMILY } from "../styles/Fonts";
|
||||
|
||||
const GradientButton = ({
|
||||
title = "",
|
||||
colors = ["#F94697", "#7023F7"],
|
||||
onPress,
|
||||
props,
|
||||
}) => {
|
||||
return (
|
||||
<Pressable onPress={onPress}>
|
||||
<LinearGradient
|
||||
colors={colors}
|
||||
style={{
|
||||
...Style.containerCenter,
|
||||
height: 50,
|
||||
borderRadius: 14,
|
||||
}}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
{...props}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 15,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default GradientButton;
|
||||
@@ -0,0 +1,24 @@
|
||||
import Hyperlink from "react-native-hyperlink";
|
||||
|
||||
import { useWebView } from "../providers/WebViewProvider";
|
||||
import { Palette } from "../styles";
|
||||
|
||||
const HyperlinkContainer = ({ children }) => {
|
||||
const { setWebViewUrl } = useWebView();
|
||||
|
||||
return (
|
||||
<Hyperlink
|
||||
onPress={(url) => {
|
||||
setWebViewUrl(url);
|
||||
}}
|
||||
linkStyle={{
|
||||
color: Palette.primary,
|
||||
textDecorationLine: "underline",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Hyperlink>
|
||||
);
|
||||
};
|
||||
|
||||
export default HyperlinkContainer;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Image, View } from "react-native";
|
||||
|
||||
import { Palette, Style } from "../styles";
|
||||
import { gutters, mainBorderRadius } from "../styles/Style";
|
||||
|
||||
const IconContainer = ({ icon }) => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: 35,
|
||||
height: 35,
|
||||
backgroundColor: Palette.transparentPrimary,
|
||||
borderRadius: mainBorderRadius,
|
||||
marginRight: gutters / 2,
|
||||
...Style.containerCenter,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={icon}
|
||||
resizeMode="contain"
|
||||
style={{
|
||||
width: "50%",
|
||||
height: "50%",
|
||||
tintColor: Palette.primary,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconContainer;
|
||||
@@ -0,0 +1,106 @@
|
||||
import React, { useState, useEffect } from "reactn";
|
||||
import { Image, Pressable, Text, View } from "react-native";
|
||||
import { FlatGrid } from "react-native-super-grid";
|
||||
|
||||
import { responsiveWidth } from "../actions/responsiveSizes.js";
|
||||
|
||||
import { Fonts, Palette, Style, gutters } from "../styles";
|
||||
import { mainBorderRadius } from "../styles/Style";
|
||||
|
||||
import useLayoutType from "../hooks/useLayoutType.js";
|
||||
|
||||
const IconSelector = ({ onClose } = {}) => {
|
||||
const { isNative } = useLayoutType();
|
||||
|
||||
const [currentIconIndex, setCurrentIconIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (isNative) {
|
||||
// import("expo-dynamic-app-icon").then((module) => {
|
||||
// getAppIcon = module.getAppIcon;
|
||||
|
||||
// const iconIndex = getAppIcon();
|
||||
// setCurrentIconIndex(Number(iconIndex));
|
||||
// });
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FlatGrid
|
||||
ListHeaderComponent={
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
...Fonts({ type: "title" }),
|
||||
textAlign: "center",
|
||||
marginBottom: gutters / 2,
|
||||
}}
|
||||
>
|
||||
Nouvelle icône
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
...Fonts({ type: "default" }),
|
||||
textAlign: "center",
|
||||
width: "60%",
|
||||
alignSelf: "center",
|
||||
marginBottom: gutters,
|
||||
}}
|
||||
>
|
||||
Modifiez l'icône de votre app, pour une expérience plus personnelle.
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
itemDimension={responsiveWidth(45)}
|
||||
data={[
|
||||
require(`../../assets/alternateIcons/1.png`),
|
||||
require(`../../assets/alternateIcons/2.png`),
|
||||
require(`../../assets/alternateIcons/3.png`),
|
||||
require(`../../assets/alternateIcons/4.png`),
|
||||
]}
|
||||
style={{ flex: 1, backgroundColor: Palette.darkPurple }}
|
||||
spacing={10}
|
||||
renderItem={({ item, index }) => {
|
||||
const isSelected = currentIconIndex === index + 1;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
key={index}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: responsiveWidth(45),
|
||||
...Style.containerCenter,
|
||||
...Style.defaultShadows,
|
||||
}}
|
||||
onPress={() => {
|
||||
// import("expo-dynamic-app-icon").then((module) => {
|
||||
// setAppIcon = module.setAppIcon;
|
||||
|
||||
// setAppIcon((index + 1).toString());
|
||||
// setCurrentIconIndex(index + 1);
|
||||
// onClose?.();
|
||||
// });
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={item}
|
||||
resizeMode="cover"
|
||||
style={{
|
||||
width: "90%",
|
||||
height: "90%",
|
||||
borderRadius: mainBorderRadius * 2,
|
||||
overflow: "hidden",
|
||||
...(isSelected && {
|
||||
borderColor: Palette.primary,
|
||||
borderWidth: 2,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconSelector;
|
||||
@@ -0,0 +1,272 @@
|
||||
import {
|
||||
Pressable,
|
||||
TextInput,
|
||||
InputAccessoryView,
|
||||
Text,
|
||||
View,
|
||||
Image,
|
||||
Keyboard,
|
||||
} from "react-native";
|
||||
import { useState } from "react";
|
||||
import CountryPicker, { DARK_THEME } from "react-native-country-picker-modal";
|
||||
import usePlaceApi from "react-native-minuit/src/hooks/usePlacesApi";
|
||||
|
||||
import { art, icons } from "../assets";
|
||||
import { Fonts, Palette, Style, gutters } from "../styles";
|
||||
import { fontTypeList } from "../styles/Fonts";
|
||||
|
||||
import { isWeb } from "../hooks/useLayoutType";
|
||||
import { GOOGLE_API_KEY } from "../data/keys";
|
||||
|
||||
const Input = ({
|
||||
inputRef = null,
|
||||
label = "",
|
||||
placeholder = "",
|
||||
|
||||
containerStyle = {},
|
||||
textInputStyle = {},
|
||||
|
||||
value,
|
||||
setValue,
|
||||
|
||||
textInputProps = {},
|
||||
|
||||
type = "default", // "default" | "password" | "textarea" | "coinAmount" | "countryPicker" | "autoCompleteAddress"
|
||||
theme = "default", // "default" | "radioactiv" | "dashed"
|
||||
|
||||
borderType = "none", // "solid" | "dashed" | "none"
|
||||
|
||||
layout = "default", // "default" | "line"
|
||||
|
||||
isNumeric = false,
|
||||
}) => {
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showCountryPicker, setShowCountryPicker] = useState(false);
|
||||
|
||||
const isDefaultLayout = layout === "default";
|
||||
|
||||
const mainColor =
|
||||
theme === "radioactiv" ? Palette.radioactivGreen : Palette.primary;
|
||||
|
||||
const isRoundedRectangle =
|
||||
["textarea", "coinAmount"].includes(type) || layout === "default";
|
||||
|
||||
const inputAccessoryViewID = "uniqueID";
|
||||
|
||||
const { places } = usePlaceApi({
|
||||
query: type === "autoCompleteAddress" ? value : "",
|
||||
apiKey: GOOGLE_API_KEY, // Your Google API Key
|
||||
queryFields: "formatted_address,geometry,name,address_components",
|
||||
queryCountries: ["fr"],
|
||||
language: "fr-FR",
|
||||
minChars: 2,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
...(isRoundedRectangle
|
||||
? {
|
||||
...Style.containerItem,
|
||||
...(isDefaultLayout
|
||||
? {
|
||||
paddingVertical: 10,
|
||||
backgroundColor:
|
||||
type === "coinAmount"
|
||||
? Palette.transparentRadioactivGreen
|
||||
: Palette.lightPurple,
|
||||
height: type === "textarea" ? 150 : "auto",
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
: {
|
||||
...Style.containerCenter,
|
||||
borderBottomColor: mainColor,
|
||||
borderBottomWidth: 1,
|
||||
}),
|
||||
...(borderType === "dashed"
|
||||
? {
|
||||
borderStyle: "dashed",
|
||||
borderColor: isFocused
|
||||
? Palette.primary
|
||||
: Palette.transparentPrimary,
|
||||
borderWidth: 1,
|
||||
}
|
||||
: {}),
|
||||
width: "100%",
|
||||
...containerStyle,
|
||||
}}
|
||||
>
|
||||
{label?.length > 0 && (
|
||||
<Text style={{ ...Fonts({ type: "default" }) }}>{label}</Text>
|
||||
)}
|
||||
|
||||
<View
|
||||
style={{
|
||||
...Style.containerRow,
|
||||
...(isRoundedRectangle ? { flex: 1 } : {}),
|
||||
paddingVertical: 10,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{type === "search" ? (
|
||||
<Image
|
||||
source={icons.search}
|
||||
style={[Style.iconDefault, { marginRight: 10 }]}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{type === "countryPicker" ? (
|
||||
<CountryPicker
|
||||
countryCode={value}
|
||||
onSelect={(country) => {
|
||||
console.log(country);
|
||||
setValue(country.cca2);
|
||||
}}
|
||||
visible={showCountryPicker}
|
||||
theme={{
|
||||
...DARK_THEME,
|
||||
primaryColor: Palette.primary,
|
||||
backgroundColor: Palette.darkPurple,
|
||||
...fontTypeList.default,
|
||||
}}
|
||||
closeButtonImage={icons.arrowRight}
|
||||
closeButtonStyle={[Style.mirrorHorizontal]}
|
||||
withEmoji={true}
|
||||
withFilter={true}
|
||||
withAlphaFilter={false}
|
||||
withCountryNameButton={true}
|
||||
containerButtonStyle={{ padding: 0, margin: 0 }}
|
||||
translation="fra"
|
||||
placeholder={"Sélectionnez un pays"}
|
||||
countryName={value}
|
||||
preferredCountries={["FR", "BE", "CH", "LU", "CA", "US", "GB"]}
|
||||
filterProps={{
|
||||
placeholder: "Rechercher un pays",
|
||||
style: {
|
||||
...Fonts({}),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={mainColor}
|
||||
value={value}
|
||||
onChangeText={setValue}
|
||||
multiline={type === "textarea"}
|
||||
editable={type !== "countryPicker"}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
style={{
|
||||
width: "100%",
|
||||
...(isRoundedRectangle
|
||||
? { height: "100%", flex: 1, textAlignVertical: "top" }
|
||||
: { textAlign: "center" }),
|
||||
...Fonts({ type: "default", color: mainColor }),
|
||||
...(isWeb
|
||||
? {
|
||||
lineHeight: "auto",
|
||||
}
|
||||
: {}),
|
||||
...textInputStyle,
|
||||
}}
|
||||
{...(type === "password"
|
||||
? {
|
||||
secureTextEntry: !showPassword,
|
||||
autoCapitalize: "none",
|
||||
autoCompleteType: "password",
|
||||
textContentType: "password",
|
||||
}
|
||||
: {})}
|
||||
{...(type === "email"
|
||||
? {
|
||||
keyboardType: "email-address",
|
||||
autoCapitalize: "none",
|
||||
autoCompleteType: "email",
|
||||
textContentType: "emailAddress",
|
||||
}
|
||||
: {})}
|
||||
keyboardType={isNumeric ? "numeric" : "default"}
|
||||
keyboardAppearance="dark"
|
||||
inputAccessoryViewID={inputAccessoryViewID}
|
||||
{...textInputProps}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === "password" ? (
|
||||
<Pressable
|
||||
onPress={() => setShowPassword(!showPassword)}
|
||||
style={{ position: "absolute", right: 0 }}
|
||||
>
|
||||
<Image
|
||||
source={icons.eye}
|
||||
style={[Style.iconDefault]}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</Pressable>
|
||||
) : type === "coinAmount" ? (
|
||||
<Image
|
||||
source={art.coin}
|
||||
style={[Style.iconDefault]}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{type === "autoCompleteAddress" &&
|
||||
places?.[0]?.description &&
|
||||
places?.[0]?.description !== value &&
|
||||
places.map((place, index) => (
|
||||
<Pressable
|
||||
key={index}
|
||||
onPress={() => {
|
||||
setValue(place?.description);
|
||||
}}
|
||||
style={{
|
||||
...Style.containerSpaceBetween,
|
||||
marginBottom: index !== places.length - 1 ? gutters / 2 : 0,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
...Fonts({}),
|
||||
}}
|
||||
>
|
||||
{place?.description || "-"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{(type === "textarea" || isNumeric) && !isWeb && (
|
||||
<InputAccessoryView nativeID={inputAccessoryViewID}>
|
||||
<Pressable
|
||||
onPress={() => Keyboard.dismiss()}
|
||||
style={{
|
||||
...Style.containerRow,
|
||||
justifyContent: "flex-end",
|
||||
backgroundColor: Palette.transparentPrimary,
|
||||
padding: gutters,
|
||||
paddingVertical: gutters / 2,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
...Fonts({ color: Palette.primary }),
|
||||
}}
|
||||
>
|
||||
Fermer
|
||||
</Text>
|
||||
</Pressable>
|
||||
</InputAccessoryView>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { Input };
|
||||
@@ -0,0 +1,116 @@
|
||||
import { useState } from "react";
|
||||
import { View, Text, Image, Pressable, StyleSheet } from "react-native";
|
||||
import { BlurView } from "expo-blur";
|
||||
|
||||
import Switch from "../components/Switch";
|
||||
import { Container, Title, Input, Button } from "../components/Dialog";
|
||||
|
||||
import { Fonts, Style, gutters } from "../styles";
|
||||
import { icons } from "../assets";
|
||||
|
||||
const labelOptions = {
|
||||
name: "Nouveau nom",
|
||||
email: "Nouvelle adresse email",
|
||||
password: "Nouveau mot de passe",
|
||||
};
|
||||
|
||||
export default ({ itemKey, type, value, title, onUpdateValue }) => {
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [inputData, setInputData] = useState(value);
|
||||
const [currentPassword, setCurrentPassword] = useState("");
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={{}}>
|
||||
<View style={Style.containerSpaceBetween}>
|
||||
<Text style={Fonts({ type: "section" })}>{title}</Text>
|
||||
{type === "boolean" ? (
|
||||
<Switch
|
||||
value={value}
|
||||
setValue={(newValue) =>
|
||||
onUpdateValue({ key: itemKey, value: newValue })
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Pressable
|
||||
onPress={() => setShowDialog(true)}
|
||||
style={[
|
||||
Style.containerRow,
|
||||
{ maxWidth: "50%", justifyContent: "flex-end" },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={Fonts({
|
||||
type: "section",
|
||||
style: {
|
||||
opacity: 0.5,
|
||||
textAlign: "right",
|
||||
width: "80%",
|
||||
marginRight: gutters,
|
||||
},
|
||||
})}
|
||||
>
|
||||
{itemKey === "password" ? "********" : value}
|
||||
</Text>
|
||||
|
||||
<Image
|
||||
source={icons.edit}
|
||||
style={Style.iconDefault}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={Style.separatorHorizontal} />
|
||||
</View>
|
||||
|
||||
<Container
|
||||
visible={showDialog}
|
||||
blurComponentIOS={
|
||||
<BlurView
|
||||
style={StyleSheet.absoluteFill}
|
||||
blurType="xdark"
|
||||
blurAmount={50}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Title>{`Changement ${title?.toLowerCase()}`}</Title>
|
||||
|
||||
{["password", "email"].includes(itemKey) && (
|
||||
<Input
|
||||
label="Mot de passe actuel"
|
||||
value={currentPassword}
|
||||
onChangeText={(text) => setCurrentPassword(text)}
|
||||
keyboardType="visible-password"
|
||||
type={itemKey}
|
||||
containerStyle={{ marginBottom: gutters / 2 }}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
label={labelOptions[itemKey]}
|
||||
value={inputData}
|
||||
onChangeText={(text) => setInputData(text)}
|
||||
autoCapitalize={
|
||||
["password", "email"].includes(itemKey) ? "none" : "words"
|
||||
}
|
||||
type={itemKey}
|
||||
containerStyle={{ marginBottom: gutters / 2 }}
|
||||
/>
|
||||
<Button
|
||||
label="Valider"
|
||||
onPress={() => {
|
||||
onUpdateValue({ key: itemKey, value: inputData, currentPassword });
|
||||
setShowDialog(false);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
label="Annuler"
|
||||
onPress={() => setShowDialog(false)}
|
||||
type={"secondary"}
|
||||
/>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from "react";
|
||||
import { Image, Pressable, Text, View } from "react-native";
|
||||
|
||||
import { responsiveHeight } from "../actions/responsiveSizes";
|
||||
|
||||
import { Fonts, Palette, Style } from "../styles";
|
||||
import { icons } from "../assets";
|
||||
import IconContainer from "./IconContainer";
|
||||
|
||||
const ItemRowList = ({
|
||||
title,
|
||||
action,
|
||||
icon = null,
|
||||
textStyle = {},
|
||||
addMarginTopFromPrevious = false,
|
||||
containerStyle = {},
|
||||
separatorPosition = "bottom",
|
||||
}) => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
...(addMarginTopFromPrevious ? { marginTop: responsiveHeight(5) } : {}),
|
||||
...containerStyle,
|
||||
}}
|
||||
>
|
||||
{separatorPosition === "top" && (
|
||||
<View style={Style.separatorHorizontal} />
|
||||
)}
|
||||
|
||||
<Pressable onPress={action} style={Style.containerSpaceBetween}>
|
||||
<View style={Style.containerRow}>
|
||||
{icon && <IconContainer icon={icon} />}
|
||||
|
||||
<Text style={Fonts({ type: "section", style: textStyle })}>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Image source={icons.arrowRight} style={Style.iconSmall} />
|
||||
</Pressable>
|
||||
|
||||
{separatorPosition === "bottom" && (
|
||||
<View style={Style.separatorHorizontal} />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemRowList;
|
||||
@@ -0,0 +1,7 @@
|
||||
import NativeLinearGradient, {
|
||||
LinearGradientProps,
|
||||
} from "react-native-linear-gradient";
|
||||
|
||||
const LinearGradient = NativeLinearGradient;
|
||||
|
||||
export { LinearGradient, LinearGradientProps };
|
||||
@@ -0,0 +1,3 @@
|
||||
import WebLinearGradient from "react-native-web-linear-gradient";
|
||||
|
||||
export const LinearGradient = WebLinearGradient;
|
||||
@@ -0,0 +1,4 @@
|
||||
import NativeMaskedView from "@react-native-masked-view/masked-view";
|
||||
|
||||
const MaskedView = NativeMaskedView;
|
||||
export default MaskedView;
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
function MaskedView({ maskElement, ...props }) {
|
||||
return React.createElement(View, props, maskElement);
|
||||
}
|
||||
|
||||
export default MaskedView;
|
||||
@@ -0,0 +1,44 @@
|
||||
import { View, Text, Image, Pressable } from "react-native";
|
||||
import React from "react";
|
||||
import { icons } from "../assets";
|
||||
import { Palette, Style } from "../styles";
|
||||
import ProgressBar from "./ProgressBar";
|
||||
import { FONT_FAMILY } from "../styles/Fonts";
|
||||
|
||||
const MusicLandHeader = ({ onPressBack, onPressSkip, showSkip = false }) => {
|
||||
return (
|
||||
<View style={{ alignItems: "center", gap: 16 }}>
|
||||
<Image source={icons.musicLandLogo} />
|
||||
<View style={{ width: "100%", ...Style.containerRow, gap: 16 }}>
|
||||
<Pressable
|
||||
style={{ width: 24, height: 24, ...Style.containerCenter }}
|
||||
onPress={onPressBack}
|
||||
>
|
||||
<Image
|
||||
source={icons.chevronDown}
|
||||
style={{ width: 15, height: 15, transform: [{ rotate: "90deg" }] }}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</Pressable>
|
||||
<View style={{ flex: 1 }}>
|
||||
<ProgressBar progress={10} />
|
||||
</View>
|
||||
{showSkip && (
|
||||
<Pressable onPress={onPressSkip}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
Passer
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default MusicLandHeader;
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Image, Pressable, View } from "react-native";
|
||||
import { Motion } from "@legendapp/motion";
|
||||
|
||||
import { icons } from "../assets";
|
||||
import { Fonts, gutters, Style } from "../styles";
|
||||
import { goBack } from "../navigation/NavigationService";
|
||||
|
||||
export default ({
|
||||
title = "",
|
||||
rightComponent = null,
|
||||
onBackPressed = null,
|
||||
containerStyle = {},
|
||||
}) => {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
Style.containerSpaceBetween,
|
||||
{
|
||||
alignItems: "flex-start",
|
||||
marginBottom: gutters / 2,
|
||||
...containerStyle,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable onPress={() => onBackPressed?.() || goBack()}>
|
||||
<Image
|
||||
source={icons.arrowRight}
|
||||
style={[Style.iconDefault, Style.mirrorHorizontal]}
|
||||
/>
|
||||
</Pressable>
|
||||
|
||||
{typeof title === "function" ? (
|
||||
title()
|
||||
) : title?.length > 0 ? (
|
||||
<Motion.Text
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0.2 }}
|
||||
transition={{
|
||||
default: {
|
||||
type: "spring",
|
||||
},
|
||||
opacity: {
|
||||
type: "timing",
|
||||
},
|
||||
}}
|
||||
style={Fonts({
|
||||
type: "navigationTitle",
|
||||
style: {
|
||||
fontSize:
|
||||
title.length > 15
|
||||
? Fonts({ type: "section" }).fontSize
|
||||
: Fonts({ type: "navigationTitle" }).fontSize,
|
||||
},
|
||||
})}
|
||||
>
|
||||
{title}
|
||||
</Motion.Text>
|
||||
) : null}
|
||||
|
||||
<View>{rightComponent?.() || null}</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
|
||||
import { Fonts } from "../styles";
|
||||
import Style, { defaultItemHeight } from "../styles/Style";
|
||||
|
||||
const OptionSelector = ({
|
||||
optionTypeList = {},
|
||||
selected,
|
||||
setSelected,
|
||||
containerStyle = {},
|
||||
colorMap = {},
|
||||
}) => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
...Style.containerSpaceBetween,
|
||||
...Style.containerItem,
|
||||
padding: 0,
|
||||
...containerStyle,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{Object.entries(optionTypeList).map(([key, value], index) => {
|
||||
const isSelected = key === selected;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
key={key}
|
||||
onPress={() => setSelected(key)}
|
||||
style={{
|
||||
...Style.containerItem,
|
||||
...Style.containerCenter,
|
||||
flex: 1,
|
||||
padding: 0,
|
||||
height: defaultItemHeight,
|
||||
margin: defaultItemHeight * 0.2,
|
||||
backgroundColor: isSelected
|
||||
? colorMap[key]?.secondary
|
||||
: "transparent",
|
||||
borderWidth: 1,
|
||||
borderColor: isSelected ? colorMap[key]?.primary : "transparent",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
...Fonts({ type: "section" }),
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default OptionSelector;
|
||||
@@ -0,0 +1,77 @@
|
||||
import React from "react";
|
||||
import { Pressable, StyleSheet } from "react-native";
|
||||
import { AnimatePresence, Motion } from "@legendapp/motion";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { Portal } from "@gorhom/portal";
|
||||
|
||||
import useLayoutType from "../hooks/useLayoutType";
|
||||
|
||||
import { Palette, Style } from "../styles";
|
||||
import { defaultBlurIntensity } from "../styles/Style";
|
||||
|
||||
const Overlay = ({
|
||||
isVisible,
|
||||
setIsVisible,
|
||||
|
||||
blurIntensity = null,
|
||||
|
||||
children,
|
||||
contentContainerStyle = {},
|
||||
|
||||
hasPortal = true,
|
||||
}) => {
|
||||
const { isNative } = useLayoutType();
|
||||
|
||||
const ContentContainerView = hasPortal ? Portal : React.Fragment;
|
||||
|
||||
return (
|
||||
<ContentContainerView>
|
||||
<AnimatePresence>
|
||||
{isVisible ? (
|
||||
<Motion.View
|
||||
key="overlay"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{
|
||||
type: "tween",
|
||||
duration: 500,
|
||||
}}
|
||||
style={{
|
||||
position: isNative ? "absolute" : "fixed",
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: Palette.ultraLightBlack,
|
||||
flex: 1,
|
||||
...Style.containerCenter,
|
||||
}}
|
||||
>
|
||||
<BlurView
|
||||
intensity={blurIntensity || isNative ? defaultBlurIntensity : 15}
|
||||
tint="dark"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
...Style.containerCenter,
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...contentContainerStyle,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
style={{
|
||||
...StyleSheet.absoluteFillObject,
|
||||
}}
|
||||
onPress={() => {
|
||||
console.log("overlay pressed");
|
||||
setIsVisible?.(false);
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</BlurView>
|
||||
</Motion.View>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
</ContentContainerView>
|
||||
);
|
||||
};
|
||||
|
||||
export default Overlay;
|
||||
@@ -0,0 +1,40 @@
|
||||
import { View } from "react-native";
|
||||
import { Motion } from "@legendapp/motion";
|
||||
|
||||
import { Palette } from "../styles";
|
||||
|
||||
export default ({
|
||||
carouselRef = null,
|
||||
selectedIndex,
|
||||
length,
|
||||
containerStyle = {},
|
||||
}) => {
|
||||
return (
|
||||
<View style={{ flexDirection: "row", ...containerStyle }}>
|
||||
{Array.from({ length }).map((_, index) => (
|
||||
<Motion.Pressable
|
||||
key={index}
|
||||
onPress={() => {
|
||||
if (carouselRef?.current && index !== selectedIndex) {
|
||||
console.log("scroll to", index);
|
||||
carouselRef.current.scrollTo({ index });
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
backgroundColor:
|
||||
index === selectedIndex
|
||||
? Palette.primary
|
||||
: Palette.ultraLightWhite,
|
||||
marginHorizontal: 5,
|
||||
}}
|
||||
animate={{
|
||||
width: index === selectedIndex ? 20 : 10,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
import React, { useGlobal } from "reactn";
|
||||
import { View, Text, Pressable } from "react-native";
|
||||
import { capitalize } from "lodash";
|
||||
import moment from "moment";
|
||||
import { Motion } from "@legendapp/motion";
|
||||
import { validateDate } from "react-native-minuit/src/actions/dateActions";
|
||||
|
||||
import { Fonts, Palette, Style } from "../styles";
|
||||
import { projectsRef } from "../config/firebase";
|
||||
|
||||
export default ({ actionList = [] }) => {
|
||||
const [currentProjectID] = useGlobal("currentProjectID");
|
||||
|
||||
const onCheck = async ({ todoID, isChecked = false }) => {
|
||||
try {
|
||||
const updatedObject = {
|
||||
isChecked: !isChecked,
|
||||
completionTimestamp: !isChecked ? new Date() : null,
|
||||
};
|
||||
|
||||
await projectsRef
|
||||
.doc(currentProjectID)
|
||||
.collection("todoList")
|
||||
.doc(todoID)
|
||||
.update(updatedObject);
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!actionList.length) {
|
||||
return (
|
||||
<Text style={Fonts({ type: "default", style: {} })}>
|
||||
Vous n'avez pas d'actions en attente.
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return actionList.map(
|
||||
(
|
||||
{
|
||||
title = "",
|
||||
requestedCompletionTimestamp = null,
|
||||
isChecked = false,
|
||||
todoID = null,
|
||||
},
|
||||
index
|
||||
) => {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<Pressable
|
||||
style={Style.containerRow}
|
||||
onPress={() => onCheck({ todoID, isChecked })}
|
||||
>
|
||||
<View style={[Style.containerRadio, { marginRight: 20 }]}>
|
||||
{isChecked && (
|
||||
<Motion.View
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
style={Style.circleRadio}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text
|
||||
style={Fonts({
|
||||
type: "default",
|
||||
style: isChecked
|
||||
? { textDecorationLine: "line-through" }
|
||||
: {},
|
||||
})}
|
||||
>
|
||||
{capitalize(title)}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
style={Fonts({
|
||||
type: "default",
|
||||
color: Palette.transparentWhite,
|
||||
})}
|
||||
>
|
||||
{capitalize(
|
||||
moment(validateDate(requestedCompletionTimestamp)).fromNow()
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
|
||||
{index !== actionList.length - 1 && (
|
||||
<View
|
||||
style={[
|
||||
Style.separatorHorizontal,
|
||||
Style.containerRevertGutter,
|
||||
{ width: "112%" },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { mainBorderRadius } from "../styles/Style";
|
||||
import { Palette } from "../styles";
|
||||
|
||||
const ProgressBar = ({ progress = 0, containerStyle = {} }) => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 5,
|
||||
backgroundColor: "#0F0C19",
|
||||
borderRadius: mainBorderRadius,
|
||||
overflow: "hidden",
|
||||
...containerStyle,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: `${progress}%`,
|
||||
height: "100%",
|
||||
backgroundColor: Palette.white,
|
||||
borderRadius: mainBorderRadius,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProgressBar;
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Image, Text, View } from "react-native";
|
||||
import { responsiveWidth } from "../actions/responsiveSizes.js";
|
||||
import { getInitials } from "../helpers";
|
||||
import { Fonts, Style } from "../styles";
|
||||
|
||||
export default ({
|
||||
name = "",
|
||||
url = null,
|
||||
size = responsiveWidth(7),
|
||||
containerStyle = {},
|
||||
}) => {
|
||||
const colors = [
|
||||
"#FB68A8",
|
||||
"#FA6DA8",
|
||||
"#F971A7",
|
||||
"#F876A7",
|
||||
"#F67BA6",
|
||||
"#F580A6",
|
||||
"#F384A5",
|
||||
"#F289A5",
|
||||
"#F08EA4",
|
||||
"#EF93A4",
|
||||
"#ED97A3",
|
||||
"#EC9CA3",
|
||||
"#EAA1A2",
|
||||
"#E8A6A2",
|
||||
"#E7AAA1",
|
||||
"#E5AFA1",
|
||||
"#E3B4A0",
|
||||
"#E2B9A0",
|
||||
"#E0BDAF",
|
||||
"#DFC2AE",
|
||||
"#DDC7AE",
|
||||
"#DBCBAE",
|
||||
"#DACFAE",
|
||||
"#D8D4AE",
|
||||
"#D6D8AE",
|
||||
"#D5DCAE",
|
||||
"#D3E1AE",
|
||||
"#D2E5AE",
|
||||
"#D0E9AE",
|
||||
"#CFEDAE",
|
||||
];
|
||||
|
||||
const pickColor = (name) => {
|
||||
const sumChars = name
|
||||
.split("")
|
||||
.reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||
const index = sumChars % 30;
|
||||
|
||||
return colors[index];
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
...Style.containerRound,
|
||||
width: size,
|
||||
height: size,
|
||||
backgroundColor: pickColor(name),
|
||||
...containerStyle,
|
||||
}}
|
||||
>
|
||||
{url ? (
|
||||
<Image
|
||||
source={{ uri: url }}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: size / 2,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Text style={Fonts({ type: "section", style: { fontSize: size / 3 } })}>
|
||||
{getInitials(name)}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useContext } from "reactn";
|
||||
import { Pressable, View, Image, Text } from "react-native";
|
||||
import { Video, ResizeMode } from "expo-av";
|
||||
|
||||
import { Fonts, gutters, Palette } from "../styles";
|
||||
import Style, { mainBorderRadius } from "../styles/Style";
|
||||
|
||||
import { icons } from "../assets";
|
||||
|
||||
import { substringTextLength } from "../helpers/index.js";
|
||||
|
||||
import { WebViewContext } from "../providers/WebViewProvider.js";
|
||||
|
||||
const thumbnailStyle = {
|
||||
width: 200,
|
||||
height: 200,
|
||||
borderRadius: mainBorderRadius / 2,
|
||||
overflow: "hidden",
|
||||
};
|
||||
|
||||
const RenderChatFile = (
|
||||
{ uri, type = "image", name = "", containerStyle = {} },
|
||||
index
|
||||
) => {
|
||||
const { setWebViewUrl } = useContext(WebViewContext);
|
||||
|
||||
return (
|
||||
<View
|
||||
key={index}
|
||||
style={{
|
||||
...Style.containerRow,
|
||||
...containerStyle,
|
||||
}}
|
||||
>
|
||||
{type === "IMAGE" ? (
|
||||
<Pressable>
|
||||
<Image
|
||||
alt={name}
|
||||
source={{
|
||||
uri,
|
||||
}}
|
||||
style={thumbnailStyle}
|
||||
/>
|
||||
</Pressable>
|
||||
) : type === "FILE" ? (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
setWebViewUrl(uri);
|
||||
}}
|
||||
style={{
|
||||
...Style.containerRow,
|
||||
...Style.containerCenter,
|
||||
backgroundColor: Palette.transparentPrimary,
|
||||
padding: gutters / 2,
|
||||
borderRadius: mainBorderRadius / 2,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
resizeMode="contain"
|
||||
source={icons.attachment}
|
||||
style={{
|
||||
...Style.iconSmall,
|
||||
marginRight: 5,
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={Fonts({
|
||||
type: "default",
|
||||
style: {
|
||||
color: Palette.white,
|
||||
textDecorationLine: "underline",
|
||||
},
|
||||
})}
|
||||
>
|
||||
{substringTextLength({ text: name, maxLength: 40 })}
|
||||
</Text>
|
||||
</Pressable>
|
||||
) : type === "VIDEO" ? (
|
||||
<Video
|
||||
style={{ ...thumbnailStyle }}
|
||||
source={{
|
||||
uri,
|
||||
}}
|
||||
// posterSource={{
|
||||
// uri: thumbnailURI,
|
||||
// }}
|
||||
// usePoster={thumbnailURI ? true : false}
|
||||
useNativeControls
|
||||
resizeMode={ResizeMode.CONTAIN}
|
||||
isLooping
|
||||
// onPlaybackStatusUpdate={status => setStatus(() => status)}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default RenderChatFile;
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from "reactn";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
import { Fonts, gutters, Style } from "../styles";
|
||||
|
||||
const SectionTextHeader = ({
|
||||
title = "",
|
||||
rightText = "",
|
||||
rightTextStyle = {},
|
||||
subTitle = "",
|
||||
containerStyle = {},
|
||||
}) => {
|
||||
return (
|
||||
<View style={{ ...containerStyle }}>
|
||||
<View
|
||||
style={{ ...Style.containerSpaceBetween, marginBottom: gutters / 2 }}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
...Fonts({
|
||||
type: "section",
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
...Fonts({
|
||||
type: "section",
|
||||
}),
|
||||
...rightTextStyle,
|
||||
}}
|
||||
>
|
||||
{rightText}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{subTitle?.length > 0 && (
|
||||
<Text
|
||||
style={{
|
||||
...Fonts({ type: "default", style: { opacity: 0.5 } }),
|
||||
marginBottom: gutters / 2,
|
||||
}}
|
||||
>
|
||||
{subTitle}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionTextHeader;
|
||||
@@ -0,0 +1,96 @@
|
||||
import React, { useRef } from "react";
|
||||
import { View, Text, Image, Animated, PanResponder } from "react-native";
|
||||
|
||||
import { Fonts, Palette, Style } from "../styles";
|
||||
import { icons } from "../assets";
|
||||
|
||||
const SlideToUnlock = ({ onUnlock, containerStyle = {} }) => {
|
||||
const { width = 0 } = containerStyle;
|
||||
const triggerPoint = width * 0.8; // Modification de la valeur du triggerPoint
|
||||
|
||||
const slideAnim = useRef(new Animated.Value(0)).current;
|
||||
|
||||
const panResponder = useRef(
|
||||
PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => true,
|
||||
onPanResponderMove: (e, gestureState) => {
|
||||
if (gestureState.dx > 0 && gestureState.dx < triggerPoint) {
|
||||
slideAnim.setValue(gestureState.dx);
|
||||
}
|
||||
},
|
||||
onPanResponderRelease: (e, gestureState) => {
|
||||
if (gestureState.dx > triggerPoint) {
|
||||
// Modification de la condition
|
||||
Animated.spring(slideAnim, {
|
||||
toValue: 0,
|
||||
useNativeDriver: false,
|
||||
onComplete: () => onUnlock(),
|
||||
}).start();
|
||||
} else {
|
||||
Animated.spring(slideAnim, {
|
||||
toValue: 0,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
}
|
||||
},
|
||||
})
|
||||
).current;
|
||||
|
||||
return (
|
||||
<View style={{ ...styles.container, ...containerStyle }}>
|
||||
<Text style={styles.text}>Slidez pour confirmer</Text>
|
||||
<Animated.View
|
||||
{...panResponder.panHandlers}
|
||||
style={[
|
||||
Style.containerCenter,
|
||||
styles.slider,
|
||||
{
|
||||
transform: [{ translateX: slideAnim }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Image
|
||||
source={icons.arrowRight}
|
||||
style={[
|
||||
Style.iconDefault,
|
||||
{ tintColor: Palette.darkRadioactivGreen },
|
||||
]}
|
||||
/>
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
height: 50,
|
||||
backgroundColor: Palette.transparentRadioactivGreen,
|
||||
borderRadius: 50,
|
||||
borderWidth: 2,
|
||||
borderStyle: "solid",
|
||||
borderColor: Palette.radioactivGreen,
|
||||
overflow: "hidden",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
slider: {
|
||||
position: "absolute",
|
||||
left: 3,
|
||||
top: 3,
|
||||
width: 40,
|
||||
height: 40,
|
||||
backgroundColor: Palette.radioactivGreen,
|
||||
borderRadius: 25,
|
||||
},
|
||||
text: {
|
||||
position: "absolute",
|
||||
...Fonts({
|
||||
color: "white",
|
||||
style: {
|
||||
fontWeight: "bold",
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export default SlideToUnlock;
|
||||
@@ -0,0 +1,16 @@
|
||||
import Slider from "@react-native-community/slider";
|
||||
|
||||
import { Palette } from "../styles";
|
||||
|
||||
export default (props) => {
|
||||
return (
|
||||
<Slider
|
||||
style={{ width: "100%" }}
|
||||
minimumTrackTintColor={Palette.primary}
|
||||
maximumTrackTintColor={Palette.lightPurple}
|
||||
thumbTintColor={Palette.primary}
|
||||
tapToSeek
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Motion } from "@legendapp/motion";
|
||||
import { Pressable, View } from "react-native";
|
||||
import { Easing } from "react-native-reanimated";
|
||||
|
||||
import { Palette } from "../styles";
|
||||
|
||||
export default ({ value, setValue, containerStyle = {} }) => {
|
||||
const currentColor = value ? Palette.green : Palette.red;
|
||||
|
||||
const switchHeight = 28;
|
||||
const borderWidth = 2;
|
||||
const internalMargin = 2;
|
||||
|
||||
const itemSize = switchHeight - borderWidth * 2 - internalMargin * 2;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={() => setValue(!value)}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: switchHeight * 2,
|
||||
height: switchHeight,
|
||||
borderColor: currentColor,
|
||||
borderWidth: borderWidth,
|
||||
borderRadius: 25,
|
||||
...containerStyle,
|
||||
}}
|
||||
>
|
||||
<Motion.View
|
||||
animate={{
|
||||
right: value ? 0 : switchHeight,
|
||||
left: value ? switchHeight : 0,
|
||||
}}
|
||||
transition={{
|
||||
type: "timing",
|
||||
duration: 300,
|
||||
easing: Easing.easing,
|
||||
}}
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: itemSize,
|
||||
height: itemSize,
|
||||
margin: internalMargin,
|
||||
borderRadius: 25,
|
||||
backgroundColor: currentColor,
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,179 @@
|
||||
import { View, Text, ScrollView, Pressable } from "react-native";
|
||||
|
||||
import { Style, Fonts, gutters, Palette } from "../styles";
|
||||
import { Input } from "./Input";
|
||||
import { mainBorderRadius } from "../styles/Style";
|
||||
|
||||
const Tableau = ({
|
||||
headings = [],
|
||||
data = [],
|
||||
|
||||
isEdit = false,
|
||||
|
||||
onUpdate = () => {},
|
||||
onPressRow = null,
|
||||
|
||||
mainColor = Palette.primary,
|
||||
transparentColor = Palette.transparentPrimary,
|
||||
}) => {
|
||||
const filteredHeadings = headings.filter(({ condition = true }) => condition);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
...Style.containerSpaceBetween,
|
||||
...Style.containerItem,
|
||||
backgroundColor: "transparent",
|
||||
padding: gutters / 2,
|
||||
paddingVertical: gutters / 4,
|
||||
}}
|
||||
>
|
||||
{filteredHeadings.map(({ title = "", flex }, index) => (
|
||||
<View style={{ flex, paddingRight: 10 }}>
|
||||
<Text
|
||||
key={index}
|
||||
style={Fonts({
|
||||
type: "default",
|
||||
style: {
|
||||
textAlign: !index
|
||||
? "left"
|
||||
: index === filteredHeadings?.length - 1
|
||||
? "right"
|
||||
: "center",
|
||||
color: mainColor,
|
||||
borderRightWidth:
|
||||
index !== filteredHeadings?.length - 1 ? 1 : 0,
|
||||
borderRightColor: transparentColor,
|
||||
},
|
||||
})}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{data?.length > 0 ? (
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{
|
||||
flex: 1,
|
||||
paddingTop: gutters,
|
||||
paddingBottom: 300,
|
||||
}}
|
||||
>
|
||||
{data.map((item, indexItemList) => {
|
||||
return (
|
||||
<>
|
||||
<Pressable
|
||||
key={indexItemList}
|
||||
onPress={() => onPressRow?.({ item })}
|
||||
style={{
|
||||
...Style.containerSpaceBetween,
|
||||
alignItems: "flex-start",
|
||||
paddingHorizontal: gutters / 2,
|
||||
}}
|
||||
>
|
||||
{filteredHeadings.map(
|
||||
(
|
||||
{ key, type = "DEFAULT", textStyle = () => {} },
|
||||
indexColumn
|
||||
) => {
|
||||
const { flex, isEditable, renderCell } =
|
||||
filteredHeadings[indexColumn];
|
||||
|
||||
const content = renderCell({
|
||||
item,
|
||||
key,
|
||||
indexItemList,
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={{ flex, paddingRight: 10 }}>
|
||||
{type === "CUSTOM" ? (
|
||||
content
|
||||
) : isEdit && isEditable ? (
|
||||
<Input
|
||||
key={indexColumn}
|
||||
placeholder={""}
|
||||
value={item?.[key] || ""}
|
||||
setValue={(value) =>
|
||||
onUpdate({
|
||||
index: indexItemList,
|
||||
key,
|
||||
value,
|
||||
})
|
||||
}
|
||||
type={!indexColumn ? "textarea" : "default"}
|
||||
borderType="dashed"
|
||||
containerStyle={{
|
||||
borderRadius: mainBorderRadius / 2,
|
||||
width: "100%",
|
||||
backgroundColor: "transparent", // 'white
|
||||
}}
|
||||
textInputStyle={{
|
||||
textAlign: !indexColumn
|
||||
? "left"
|
||||
: indexColumn === filteredHeadings?.length - 1
|
||||
? "right"
|
||||
: "center",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
key={indexColumn}
|
||||
style={Fonts({
|
||||
type: "default",
|
||||
style: {
|
||||
textAlign: !indexColumn ? "left" : "center",
|
||||
...textStyle({ item }),
|
||||
},
|
||||
})}
|
||||
>
|
||||
{content}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</Pressable>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: transparentColor,
|
||||
height: 1,
|
||||
marginVertical: gutters / 2,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
...Style.containerCenter,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={Fonts({
|
||||
type: "default",
|
||||
style: {
|
||||
textAlign: "center",
|
||||
color: mainColor,
|
||||
},
|
||||
})}
|
||||
>
|
||||
Il n'y a pas encore de données
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tableau;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { View } from "react-native";
|
||||
import { useState } from "react";
|
||||
import { Motion } from "@legendapp/motion";
|
||||
|
||||
import { Palette, Style } from "../styles";
|
||||
|
||||
export default () => {
|
||||
const [animatedDotIndex, setAnimatedDotIndex] = useState(0);
|
||||
|
||||
setTimeout(() => {
|
||||
setAnimatedDotIndex((animatedDotIndex + 1) % 3);
|
||||
}, 500);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
Style.containerSpaceBetween,
|
||||
{ width: 60, padding: 10, paddingVertical: 5 },
|
||||
]}
|
||||
>
|
||||
{[1, 2, 3].map((item) => (
|
||||
<Motion.View
|
||||
key={item}
|
||||
animate={{
|
||||
opacity: animatedDotIndex === item - 1 ? 1 : 0.2,
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: Palette.white,
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 50,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import { WebView as NativeWebView } from "react-native-webview";
|
||||
|
||||
export const WebView = NativeWebView;
|
||||
@@ -0,0 +1,39 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
|
||||
export const WebView = ({ source, allowFullScreen, onURLChange }) => {
|
||||
const iframeRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Définition de la fonction handleLoad à l'intérieur du useEffect
|
||||
const handleLoad = () => {
|
||||
try {
|
||||
const currentUrl = iframeRef.current?.contentWindow?.location?.href;
|
||||
onURLChange(currentUrl); // Appeler la fonction de callback avec l'URL actuelle
|
||||
} catch (error) {
|
||||
console.error("Erreur d'accès à l'URL de l'iframe:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const iframeElement = iframeRef.current;
|
||||
iframeElement.addEventListener("load", handleLoad);
|
||||
|
||||
return () => {
|
||||
iframeElement.removeEventListener("load", handleLoad);
|
||||
};
|
||||
}, [onURLChange]); // Assurez-vous que cette dépendance est correctement définie pour éviter des appels excessifs
|
||||
|
||||
return (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={source?.uri}
|
||||
style={{
|
||||
display: "flex",
|
||||
border: "none",
|
||||
width: "100%",
|
||||
height: "100vh",
|
||||
}}
|
||||
title="minuit.starter"
|
||||
allowFullScreen={allowFullScreen}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user