This commit is contained in:
Philip Cesar Garay
2025-07-31 21:25:33 +08:00
parent 5cfbc81e3a
commit 84fc719a10
307 changed files with 46470 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
import React, { useState } from "react";
import { Alert, Platform, View, Text } from "react-native";
import { PortalProvider } from "@gorhom/portal";
import { Fonts, Style, gutters } from "../styles";
import Overlay from "./Overlay";
import Button from "./Button";
const WebAlertModal = ({ title, description, options }) => {
const [visible, setVisible] = useState(true);
const confirmOption = options?.find(({ style }) => style !== "cancel");
const cancelOption = options?.find(({ style }) => style === "cancel");
const onConfirm = () => {
setVisible(false);
confirmOption?.onPress();
};
const onCancel = () => {
setVisible(false);
cancelOption?.onPress();
};
return (
<PortalProvider>
<Overlay isVisible={visible}>
<View
style={{
...Style.containerModal,
}}
>
<Text
style={{
...Fonts({
type: "mainTitle",
style: {
textAlign: "center",
marginBottom: gutters / 2,
},
}),
}}
>
{title}
</Text>
<Text
style={{
...Fonts({
type: "default",
style: {
textAlign: "center",
marginBottom: gutters / 2,
},
}),
}}
>
{description}
</Text>
<Button
text={confirmOption?.text || "OK"}
onPress={onConfirm}
alternateAction={
cancelOption
? {
text: cancelOption.text,
onPress: () => onCancel(),
}
: null
}
/>
</View>
</Overlay>
</PortalProvider>
);
};
const alertPolyfill = (title, description, options, extra) => {
const rootDiv = document.createElement("div");
document.body.appendChild(rootDiv);
const closeModal = () => {
document.body.removeChild(rootDiv);
};
const WebAlertComponent = () => (
<WebAlertModal
title={title}
description={description}
options={options}
onDismiss={closeModal}
/>
);
// Render the React component into the div
require("react-dom").render(<WebAlertComponent />, rootDiv);
};
const customAlert = (title, description, options, extra) => {
// Utilisation de la propriété userInterfaceStyle pour le mettre en mode sombre
Alert.alert(title, description, options, {
...extra,
userInterfaceStyle: "dark",
});
};
const alert = Platform.OS === "web" ? alertPolyfill : customAlert;
export default alert;
+74
View File
@@ -0,0 +1,74 @@
import { Text, View } from "react-native";
import { Image } from "expo-image";
import { responsiveWidth } from "../actions/responsiveSizes.js";
import { formatImageURL, getInitials } from "../helpers";
import { Fonts, Palette, Style } from "../styles";
import Badge from "./Badge.js";
export default ({
name = "",
url = null,
size = responsiveWidth(7),
containerStyle = {},
forceRawImage = false,
badge = {},
}) => {
const uniqColorById = (uniqId = "test") => {
// Calculer un nombre unique à partir de l'ID de l'employé
let uniqueNumber = 0;
for (let i = 0; i < uniqId?.length; i++) {
uniqueNumber += uniqId.charCodeAt(i);
}
// génère des couleurs pastels claires
const color = `hsl(${uniqueNumber % 360}, 100%, 90%)`;
return color;
};
return (
<View
style={{
width: size,
height: size,
borderRadius: 500,
...containerStyle,
}}
>
<View
style={{
...Style.containerRound,
width: "100%",
height: "100%",
backgroundColor: url ? "transparent" : uniqColorById(name),
}}
>
{url ? (
<Image
cachePolicy={"memory"}
source={{
uri: forceRawImage ? url : formatImageURL({ url, size: 200 }),
}}
style={{
width: size,
height: size,
borderRadius: size / 2,
}}
/>
) : (
<Text
style={Fonts({
type: "section",
color: Palette.darkPurple,
style: { fontSize: size / 3 },
})}
>
{getInitials(name)}
</Text>
)}
</View>
<Badge {...badge} />
</View>
);
};
+49
View File
@@ -0,0 +1,49 @@
import { Motion } from "@legendapp/motion";
import { Fonts, Palette, Style } from "../styles";
const Badge = ({
count = 0,
size = 20,
customContent = null,
backgroundColor = null,
} = {}) => {
if (!count && !customContent) {
return null;
}
return (
<Motion.View
animate={{ scale: 1 }}
initial={{ scale: 0 }}
transition={{ type: "tween", duration: 0.5 }}
style={{
position: "absolute",
top: -size / 4,
right: -size / 4,
backgroundColor: backgroundColor || Palette.red,
borderRadius: 500,
...Style.containerCenter,
width: size,
height: size,
}}
>
{customContent ? (
customContent()
) : (
<Motion.Text
animate={{ scale: 1 }}
initial={{ scale: 0 }}
transition={{ type: "tween", duration: 0.5 }}
style={{
...Fonts({ color: Palette.white }),
}}
>
{count}
</Motion.Text>
)}
</Motion.View>
);
};
export default Badge;
+61
View File
@@ -0,0 +1,61 @@
import React from "reactn";
import { Image, Pressable, Text, View } from "react-native";
import { responsiveWidth } from "../actions/responsiveSizes.js";
import { icons } from "../assets";
import { Fonts, Style, gutters } from "../styles";
import { Routes } from "../navigation";
import { navigate } from "../navigation/NavigationService";
import useLayoutType, { sidebarWidth } from "../hooks/useLayoutType.js";
export default ({ title = "", containerStyle = {} }) => {
const { isDesktop } = useLayoutType();
return (
<View
style={[
Style.containerSpaceBetween,
{ marginBottom: 20, ...containerStyle },
]}
>
<View style={Style.containerRow}>
<Text
numberOfLines={1}
style={{
...Fonts({
type: "mainTitle",
style: {
marginRight: 10,
maxWidth: isDesktop
? sidebarWidth - 4 * gutters
: responsiveWidth(70),
},
}),
}}
>
{title}
</Text>
</View>
<View style={{ ...Style.containerRow }}>
<Pressable onPress={() => navigate(Routes.Notifications)}>
<Image
resizeMode="contain"
source={icons.bell}
style={{
...Style.iconDefault,
}}
/>
<View
style={{
...Style.itemBadge,
}}
/>
</Pressable>
</View>
</View>
);
};
@@ -0,0 +1,26 @@
import { StyleSheet, View } from "react-native";
import { GradientBorderView } from "../GradientBorderView";
const BorderGradient = ({ children, gradientProps, ...props }) => {
return (
<GradientBorderView
gradientProps={{
start: { x: 0, y: 0 },
end: { x: 1, y: 0 },
...gradientProps
}}
{...props}>
<View style={styles.innerContainer}>{children}</View>
</GradientBorderView>);
};
export default BorderGradient;
const styles = StyleSheet.create({
innerContainer: {
flex: 1,
}
});
@@ -0,0 +1,135 @@
import { StyleSheet, View } from "react-native";
import { useState } from "react";
const BorderGradient = ({ children, gradientProps, ...props }) => {
const defaultGradientProps = {
start: {
x: 0.5,
y: 0
},
end: {
x: 0.5,
y: 1
},
locations: [],
colors: [],
useAngle: false,
angle: 0
};
const { locations, end, start, useAngle, angle, onLayout, colors } =
gradientProps;
const {
style,
borderWidth,
borderRadius,
borderTopRightRadius,
borderTopLeftRadius,
borderBottomLeftRadius,
borderBottomRightRadius,
borderTopWidth,
borderLeftWidth,
borderRightWidth,
borderBottomWidth
} = props;
const propStart = start ?? defaultGradientProps?.start;
const propEnd = end ?? defaultGradientProps?.end;
const [state, setState] = useState({
width: 1,
height: 1
});
const measure = (event) => {
setState({
width: event.nativeEvent.layout.width,
height: event.nativeEvent.layout.height
});
if (onLayout) {
onLayout(event);
}
};
const getAngle = () => {
if (useAngle) {
return angle + "deg";
}
// Math.atan2 handles Infinity
const _angle =
Math.atan2(
state.width * (propEnd.y - propStart.y),
state.height * (propEnd.x - propStart.x)
) +
Math.PI / 2;
return _angle + "rad";
};
const getColors = () =>
colors.
map((color, index) => {
const location = locations?.[index] ?? defaultGradientProps.locations;
let locationStyle = "";
if (location) {
locationStyle = " " + location * 100 + "%";
}
return color + locationStyle;
}).
join(",");
return (
<View
style={{
position: "relative"
}}>
<View
onLayout={measure}
style={[
style,
{
borderWidth,
borderRadius,
borderTopLeftRadius,
borderTopRightRadius,
borderBottomLeftRadius,
borderBottomRightRadius,
borderTopWidth,
borderLeftWidth,
borderRightWidth,
borderBottomWidth,
borderStyle: "solid",
borderColor: "transparent",
// borderImage: `linear-gradient(${getAngle()},${getColors()}) 1`,
background: `linear-gradient(${getAngle()},${getColors()}) border-box`,
WebkitMask:
"linear-gradient(#fff 0 0) padding-box,linear-gradient(#fff 0 0)",
WebkitMaskComposite: "xor",
maskComposite: "exclude",
overflow: "hidden"
}]
}>
</View>
<View style={styles.innerContainer}>{children}</View>
</View>);
};
export default BorderGradient;
const styles = StyleSheet.create({
innerContainer: {
flex: 1,
margin: 5, // <-- Border Width
position: "absolute",
top: 0,
bottom: 0,
alignItems: "center",
justifyContent: "center",
right: 0,
left: 0,
overflow: "hidden"
}
});
+54
View File
@@ -0,0 +1,54 @@
import { View, Text, Pressable } from "react-native";
import React from "react";
import BorderGradient from "./BorderGradient/BorderGradient";
import { Palette, Style } from "../styles";
import { BlurView } from "expo-blur";
import { FONT_FAMILY } from "../styles/Fonts";
const BorderGradientButton = () => {
return (
<Pressable>
<BorderGradient
gradientProps={{
colors: ["#F94697", "#7023F7"],
}}
style={{
height: 50,
borderRadius: 14,
borderWidth: 1,
}}
>
<View
style={{
borderRadius: 14,
overflow: "hidden",
backgroundColor: "#73737324",
}}
>
<BlurView
intensity={30}
tint="dark"
style={{
width: "100%",
height: "100%",
...Style.containerCenter,
borderRadius: 14,
}}
>
<Text
style={{
fontSize: 15,
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
}}
>
Jai déjà mes paroles
</Text>
</BlurView>
</View>
</BorderGradient>
</Pressable>
);
};
export default BorderGradientButton;
+3
View File
@@ -0,0 +1,3 @@
import BottomSheet from "@gorhom/bottom-sheet";
export default BottomSheet;
+94
View File
@@ -0,0 +1,94 @@
import React, { useImperativeHandle, useState, useRef } from "react";
import { View, TouchableOpacity, ScrollView } from "react-native";
import { Portal } from "@gorhom/portal";
import { Motion } from "@legendapp/motion";
import { Palette } from "../../styles";
import {
isDesktop,
isLargeDesktop,
sidebarWidth,
} from "../../hooks/useLayoutType";
export const SheetScrollView = ScrollView;
export const SheetBackdrop = View;
const BottomSheet = React.forwardRef((props, ref) => {
const [showSheet, setShowSheet] = useState(false);
const bottomSheetRef = useRef();
useImperativeHandle(ref, () => ({
snapToIndex: () => {
setShowSheet(true);
},
expand: () => {
setShowSheet(true);
},
collapse: () => closeBottomSheet(),
close: () => closeBottomSheet(),
}));
const closeBottomSheet = () => {
setShowSheet(false);
props.onChange(-1);
};
if (!showSheet) {
return null;
}
return (
<Portal>
<Motion.View
initial={{ right: -500, opacity: 0 }}
animate={{ right: 0, opacity: 1 }}
style={{
position: "fixed",
right: 0,
top: 0,
left: 0,
bottom: 0,
flex: 1,
zIndex: 1000000,
justifyContent: "center",
alignItems: "center",
}}
>
<TouchableOpacity
onPress={closeBottomSheet}
ref={bottomSheetRef}
style={{
position: "absolute",
right: 0,
top: 0,
left: 0,
bottom: 0,
flex: 1,
backgroundColor: "rgba(0,0,0,0.4)",
}}
/>
<View
style={{
position: "absolute",
top: 0,
right: 0,
bottom: 0,
height: "100%",
width: isDesktop
? sidebarWidth * (isLargeDesktop ? 2 : 1.5)
: "100%",
backgroundColor: Palette.lightPurple,
overflow: "scroll",
}}
>
{props.children}
</View>
</Motion.View>
</Portal>
);
});
// TODO une croix pour fermer sur mobile web
export default BottomSheet;
+87
View File
@@ -0,0 +1,87 @@
import React, { useState, useCallback, useEffect } from "react";
import { Keyboard, StyleSheet } from "react-native";
import { AnimatePresence, Motion } from "@legendapp/motion";
import { useKeyboard } from "@react-native-community/hooks";
import BottomSheet from "./BottomSheet";
import { Palette } from "../styles";
import useLayoutType from "../hooks/useLayoutType";
export default ({
children,
bottomSheetRef,
snapPoints = ["25%", "50%"],
handleStyle = {},
...rest
}) => {
const [currentSnapPointIndex, setCurrentSnapPointIndex] = useState(0);
const { keyboardShown = false } = useKeyboard();
const { isWeb } = useLayoutType();
const handleSheetChanges = useCallback((index) => {
setCurrentSnapPointIndex(index);
if (index <= 0) {
Keyboard.dismiss();
}
}, []);
useEffect(() => {
if (bottomSheetRef.current && !isWeb && currentSnapPointIndex > 0) {
if (keyboardShown) {
bottomSheetRef.current.expand();
} else {
bottomSheetRef.current.snapToIndex(1);
}
}
}, [keyboardShown]);
return (
<>
<AnimatePresence>
{currentSnapPointIndex > 0 ? (
<Motion.Pressable
style={{ ...StyleSheet.absoluteFill }}
onPress={() => bottomSheetRef?.current?.close()}
>
<Motion.View
key={"A"}
style={{
flex: 1,
backgroundColor: Palette.black,
}}
initial={{ opacity: 0 }}
animate={{ opacity: 0.9 }}
exit={{ opacity: 0 }}
transition={{
default: {
type: "spring",
},
opacity: {
type: "timing",
},
}}
></Motion.View>
</Motion.Pressable>
) : null}
</AnimatePresence>
<BottomSheet
ref={bottomSheetRef}
snapPoints={snapPoints}
handleStyle={{
backgroundColor: Palette.darkPurple,
...handleStyle,
}}
handleIndicatorStyle={{ backgroundColor: Palette.primary }}
style={{ zIndex: 100 }}
onChange={handleSheetChanges}
{...rest}
>
{children}
</BottomSheet>
</>
);
};
+151
View File
@@ -0,0 +1,151 @@
import { Motion } from "@legendapp/motion";
import { Text, View } from "react-native";
import * as Haptics from "expo-haptics";
import { Fonts, Palette } from "../styles";
import Style, { gutters, mainBorderRadius } from "../styles/Style";
import useLayoutType, {
isDesktop,
isMobile,
isNative,
} from "../hooks/useLayoutType";
export default ({
type = "primary",
theme = "default", // "default" | "radioactiv"
text,
onPress = () => console.log("null"),
isAbsoluteBottom = false,
alternateAction = {},
contentContainerStyle = {},
containerStyle = {},
textStyle = {},
isMainDesktopPanel = false,
}) => {
const buttonWidth = alternateAction?.text ? "49%" : "100%";
return (
<View
style={{
flexDirection: "row",
justifyContent: alternateAction?.text ? "space-between" : "center",
...(isAbsoluteBottom
? {
position: "absolute",
bottom: gutters * (isMobile ? 2 : 1),
alignItems: "flex-end",
...(isDesktop && !isMainDesktopPanel
? {
maxWidth: 600,
minWidth: 400,
alignSelf: "center",
}
: {
right: gutters,
left: gutters,
alignSelf: "center",
}),
}
: {
width: "100%",
}),
...contentContainerStyle,
}}
>
{alternateAction?.text && alternateAction?.onPress ? (
<BaseButton
type={"secondary"}
theme={alternateAction?.theme || theme}
text={alternateAction.text}
onPress={alternateAction.onPress}
textStyle={{
...(alternateAction?.textStyle || {}),
}}
containerStyle={{
width: buttonWidth,
...(alternateAction?.containerStyle || {}),
}}
/>
) : null}
<BaseButton
type={type}
theme={theme}
text={text}
onPress={onPress}
containerStyle={{
width: buttonWidth,
...containerStyle,
}}
textStyle={{
...textStyle,
}}
/>
</View>
);
};
export const BaseButton = ({
type = "primary",
theme = "default",
text,
onPress = () => console.log("null"),
containerStyle = {},
textStyle = {},
hasShadow = false,
}) => {
const primaryColor =
theme === "radioactiv" ? Palette.radioactivGreen : Palette.primary;
const primaryTransparentColor =
theme === "radioactiv"
? Palette.transparentRadioactivGreen
: Palette.transparentPrimary;
const textColor = type === "secondary" ? primaryColor : Palette.darkPurple;
return (
<Motion.Pressable
whileTap={{ scale: 0.8 }}
onPress={() => {
if (isNative) {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
onPress();
}}
style={{
width: "100%",
height: 50,
marginTop: gutters / 2,
...Style.containerRow,
...Style.containerCenter,
backgroundColor: primaryColor,
borderRadius: mainBorderRadius,
...(type === "secondary"
? {
backgroundColor: primaryTransparentColor,
}
: hasShadow
? { ...Style.defaultShadows }
: {}),
...containerStyle,
}}
>
<Text
style={{
...Fonts({
type: "default",
color: textColor,
}),
...textStyle,
}}
>
{text}
</Text>
</Motion.Pressable>
);
};
+141
View File
@@ -0,0 +1,141 @@
import { useState, useEffect } from "reactn";
import { Pressable, TextInput, View, Image } from "react-native";
import { BlurView } from "expo-blur";
import { Fonts, gutters, Palette } from "../styles";
import Style from "../styles/Style";
import { chatsRef } from "../config/firebase";
import { icons } from "../assets";
import { isWeb } from "../hooks/useLayoutType.js";
import DocumentDropZone from "./DocumentDropZone.js";
import FilesPreview from "./FilesPreview.js";
const ChatInput = ({
message,
setMessage,
onSendMessage,
containerStyle = {},
placeholder = "",
chatID = null,
}) => {
const [files, setFiles] = useState([]);
useEffect(() => {
setFiles([]);
}, [chatID]);
const handleMessageObject = () => {
if (message.length > 0 || files.length > 0) {
onSendMessage({
customPayload: {
files,
},
});
setMessage("");
setFiles([]);
}
};
const handleKeyPress = (e) => {
if (e?.nativeEvent?.key?.toLowerCase() === "enter") {
handleMessageObject();
}
};
return (
<View
style={{
...Style.containerItem,
backgroundColor: Palette.transparentDarkPurple,
justifyContent: "center",
borderWidth: 1,
borderColor: Palette.ultraLightWhite,
overflow: "hidden",
width: "100%",
height: "auto",
padding: 0,
...containerStyle,
}}
>
<BlurView
intensity={30}
tint="dark"
style={{ flex: 1, justifyContent: "center" }}
>
<FilesPreview
files={files}
setFiles={setFiles}
containerStyle={{ margin: gutters / 2, marginBottom: 0 }}
/>
<View style={{ ...Style.containerRow, height: 50 }}>
<DocumentDropZone
documentID={chatID}
collectionRef={chatsRef}
shouldReturnObject
setFiles={setFiles}
customElement={() => (
<View style={{ width: 50, ...Style.containerCenter }}>
<Image
resizeMode="contain"
source={icons.addFile}
style={{
...Style.iconSmall,
}}
/>
</View>
)}
/>
<TextInput
numberOfLines={1}
placeholder={placeholder}
placeholderTextColor={Palette.white}
value={message}
onChangeText={setMessage}
style={{
width: "85%",
...Fonts({ type: "default", style: {} }),
}}
keyboardAppearance="dark"
{...(!isWeb
? {
returnKeyType: "send",
onSubmitEditing: onSendMessage,
}
: {
onKeyPress: handleKeyPress,
})}
/>
<Pressable
onPress={handleMessageObject}
style={{
position: "absolute",
top: 0,
right: 0,
bottom: 0,
width: 50,
justifyContent: "center",
alignItems: "center",
}}
>
<Image
resizeMode="contain"
source={icons.send}
style={{
...Style.iconSmall,
}}
/>
</Pressable>
</View>
</BlurView>
</View>
);
};
export default ChatInput;
+294
View File
@@ -0,0 +1,294 @@
import { useState, useRef, useEffect, useGlobal, getGlobal } from "reactn";
import {
Pressable,
TextInput,
View,
Image,
Text,
Keyboard,
FlatList,
} from "react-native";
import { responsiveHeight } from "../actions/responsiveSizes.js";
import { useDataFromRef } from "react-native-minuit/src/hooks";
import { useKeyboard } from "@react-native-community/hooks";
import moment from "moment";
import { Fonts, gutters, Palette } from "../styles";
import Style, { bubbleStyle } from "../styles/Style";
import firebase, { chatsRef } from "../config/firebase";
import { formatNameForConfidentiality } from "../helpers/index.js";
import useLayoutType from "../hooks/useLayoutType.js";
import TypingLoader from "./TypingLoader";
import Avatar from "./Avatar";
import RenderChatFile from "./RenderChatFile.js";
import HyperlinkContainer from "./HyperlinkContainer.js";
export default ({
chatID = null,
layout = "default", // default | taskSideBar
containerStyle = {},
messageListContainerStyle = {},
}) => {
const [currentUID] = useGlobal("currentUID");
const [currentProjectData] = useGlobal("currentProjectData");
const [isTyping] = useState(false);
const flatListRef = useRef();
const { isNative } = useLayoutType();
const { keyboardShown = false } = useKeyboard();
const { data: messageList } = useDataFromRef({
ref: chatID
? chatsRef
.doc(chatID)
.collection("messages")
.orderBy("createdAt", "desc")
.limit(50)
: null,
simpleRef: false,
listener: true,
condition: chatID,
refreshArray: [chatID],
documentID: "messageID",
});
let conversation = [
isTyping ? { senderID: "minuit.ai", userTyping: true } : null,
...(messageList || []),
].filter((item) => item);
if (layout === "default") {
conversation = conversation.reverse();
}
useEffect(() => {
if (flatListRef?.current && isNative && keyboardShown) {
flatListRef?.current?.scrollToEnd?.({ animated: true });
}
}, [keyboardShown, isNative]);
return (
<View
style={{
height: responsiveHeight(85, true),
...containerStyle,
}}
>
<View style={{ flex: 1, ...messageListContainerStyle }}>
<FlatList
ref={flatListRef}
data={conversation}
estimatedItemSize={50}
scrollEnabled
contentContainerStyle={{
paddingBottom: responsiveHeight(40),
}}
showsVerticalScrollIndicator={false}
keyExtractor={(item, index) =>
item?.messageID
? `${item?.messageID?.toString()}-${index}`
: `no-messageID-${index}`
}
renderItem={({
item: {
createdAt = null,
senderID,
senderName = "",
senderProfilePicture = null,
text = "",
userTyping = false,
files = [],
},
index,
}) => {
const isCurrentUser = senderID === currentUID;
const isChatbot = senderID === "minuit.ai";
const senderData =
currentProjectData?.teamMembers?.[senderID] || {};
const isDayChange =
moment(createdAt?.toDate()).format("DD/MM/YYYY") !==
moment(
conversation[index - 1]?.createdAt?.toDate() || new Date()
).format("DD/MM/YYYY") || !conversation[index - 1];
return (
<>
{isDayChange && (
<View
style={{
...Style.containerCenter,
marginBottom: gutters / 2,
}}
>
<Text
style={{
...Fonts({
type: "default",
color: Palette.white,
style: { textAlign: "center", opacity: 0.5 },
}),
}}
>
{moment(createdAt?.toDate()).format(
"[Le] DD/MM/YYYY [à] HH:mm"
)}
</Text>
<View
style={{
...Style.separatorHorizontal,
width: "100%",
}}
/>
</View>
)}
<View
style={[
Style.containerRow,
{
flexDirection: isCurrentUser ? "row-reverse" : "row",
alignItems: "flex-end",
width: "100%",
marginBottom: gutters / 2,
},
]}
>
{!isCurrentUser && (
<View
style={{
...Style.containerRound,
...Style.containerCenter,
...(isCurrentUser
? {
marginLeft: gutters / 2,
backgroundColor: Palette.primary,
}
: {
marginRight: gutters / 2,
backgroundColor: "transparent",
borderColor: Palette.primary,
borderWidth: 1,
}),
}}
>
<Avatar
name={
isChatbot ? "m" : senderData?.name || senderName || ""
}
url={
senderData?.profilePictureURL ||
senderProfilePicture ||
null
}
size={35}
/>
</View>
)}
<View
style={{
alignItems: isCurrentUser ? "flex-end" : "flex-start",
}}
>
{files?.map((props, index) => (
<RenderChatFile
key={index}
{...props}
containerStyle={{
marginBottom:
index !== files?.length - 1
? gutters / 2
: text?.length > 0 || userTyping
? gutters / 2
: 0,
}}
/>
)) || null}
{(text?.length > 0 || userTyping) && (
<View
style={{
...bubbleStyle({
isCurrentUser,
customStyle: {
marginBottom: files?.length > 0 ? gutters / 2 : 0,
},
}),
}}
>
{userTyping ? (
<TypingLoader />
) : (
<HyperlinkContainer>
<Text
style={Fonts({
type: "default",
style: {
color: Palette.white,
textAlign: isCurrentUser ? "right" : "left",
width: "100%",
},
})}
>
{text}
</Text>
</HyperlinkContainer>
)}
</View>
)}
</View>
</View>
</>
);
}}
/>
</View>
</View>
);
};
export const onSendMessage = async ({
chatID = null,
projectID = null,
message = "",
setMessage = () => {},
setIsTyping = () => {},
customPayload = {},
}) => {
try {
const currentUID = getGlobal()?.currentUID || null;
const { name = "", profilePictureURL = null } =
getGlobal()?.currentUserData || {};
if (message.length > 0 || customPayload?.files?.length > 0) {
Keyboard.dismiss();
setMessage("");
const messageData = {
projectID,
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
senderID: currentUID,
senderName: formatNameForConfidentiality({ name }),
senderProfilePicture: profilePictureURL || null,
text: message,
...customPayload,
};
await chatsRef.doc(chatID).collection("messages").add(messageData);
}
} catch (error) {
console.log(error);
} finally {
setIsTyping(false);
}
};
+8
View File
@@ -0,0 +1,8 @@
import Dialog from "react-native-dialog";
export const Container = Dialog.Container;
export const Button = Dialog.Button;
export const Title = Dialog.Title;
export const Input = Dialog.Input;
export const Description = Dialog.Description;
+77
View File
@@ -0,0 +1,77 @@
import React from "react";
import { Text, View } from "react-native";
import { Input as MinuitInput } from "../Input";
import { BaseButton as MinuitButton } from "../Button";
import Overlay from "../Overlay";
import { Fonts, Palette, gutters } from "../../styles";
import Style from "../../styles/Style";
export const Container = ({ visible, setVisible = () => null, children }) => {
return (
<Overlay isVisible={visible}>
<View style={{ ...Style.containerModal }}>{children}</View>
</Overlay>
);
};
export const Title = ({ children }) => {
return (
<Text
style={{
...Fonts({
type: "title",
style: {
textAlign: "center",
marginBottom: gutters,
},
}),
}}
>
{children}
</Text>
);
};
export const Description = ({ children }) => {
return (
<Text
style={{
...Fonts({
type: "default",
style: {
textAlign: "center",
marginBottom: gutters / 2,
},
}),
}}
>
{children}
</Text>
);
};
export const Button = ({
label,
onPress,
type = "primary",
containerStyle = {},
}) => {
return (
<MinuitButton
containerStyle={{
width: "100%",
alignSelf: "center",
...containerStyle,
}}
text={label}
onPress={onPress}
type={type}
/>
);
};
export const Input = (props) => {
return <MinuitInput {...props} setValue={props.onChangeText} />;
};
+405
View File
@@ -0,0 +1,405 @@
import React, {
useCallback,
useEffect,
useRef,
useState,
useGlobal,
} from "reactn";
import { Image, Pressable, Text, View } from "react-native";
import * as ImagePicker from "expo-image-picker";
import moment from "moment";
import { useActionSheet } from "@expo/react-native-action-sheet";
import * as DocumentPicker from "expo-document-picker";
import Compressor from "react-native-compressor";
import useMinuit from "react-native-minuit/src/hooks/useMinuit";
import { arrayUnion } from "../config/firebase";
import Style, { gutterConstant, mainBorderRadius } from "../styles/Style";
import { Fonts, Palette } from "../styles";
import { icons } from "../assets";
import { uploadFileToFirebase } from "../helpers/uploadToFirebase";
import useLayoutType from "../hooks/useLayoutType";
const NATIVE_OPTIONS = [
"Importer depuis la galerie",
"Ajouter un fichier",
"Prendre une photo ou une vidéo",
"Annuler",
];
const DocumentDropZone = ({
containerStyle = {},
documentID = null,
documentExists = false,
collectionRef = null,
setFiles = null,
customElement = null,
shouldReturnObject = false,
}) => {
const dropRef = useRef(null);
const [isDragging, setIsDragging] = useState(false);
const { setIsLoading, setTooltip } = useMinuit();
const { isDesktop, isWeb, isNative } = useLayoutType();
const { showActionSheetWithOptions } = useActionSheet();
useEffect(() => {
if (isWeb && dropRef.current) {
const el = dropRef.current;
const handleDragIn = (e) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
};
const handleDragOut = (e) => {
e.preventDefault();
e.stopPropagation();
if (!dropRef.current.contains(e.relatedTarget)) {
console.log("drag left");
setIsDragging(false);
}
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
let files = [...e.dataTransfer.files];
if (files.length > 0) {
for (const file of files) {
handleWebFile({ file });
}
}
};
el.addEventListener("dragenter", handleDragIn);
el.addEventListener("dragleave", handleDragOut);
el.addEventListener("dragover", (e) => e.preventDefault());
el.addEventListener("drop", handleDrop);
return () => {
el.removeEventListener("dragenter", handleDragIn);
el.removeEventListener("dragleave", handleDragOut);
el.removeEventListener("dragover", (e) => e.preventDefault());
el.removeEventListener("drop", handleDrop);
};
}
}, [dropRef?.current]);
const handleWebFile = ({ file }) => {
try {
const { type = "" } = file;
const reader = new FileReader();
reader.onloadend = () => {
const base64 = reader.result.split(",")[1]; // Vous pourriez avoir besoin de cette valeur base64 pour un upload direct
const uri = reader.result; // URI en base64 du fichier
console.log("file", file);
onUploadDocument({ files: [{ name: file.name, uri, type }] });
};
reader.onerror = (err) => {
console.error("FileReader error", err);
};
reader.readAsDataURL(file); // Lire le fichier et déclencher reader.onloadend lorsque c'est fait
} catch (error) {
console.log(error);
setTooltip({ text: error.message, type: "error" });
}
};
const onAddFile = async ({} = {}) => {
try {
if (isNative) {
const cancelButtonIndex = NATIVE_OPTIONS.length - 1;
showActionSheetWithOptions(
{
options: NATIVE_OPTIONS,
cancelButtonIndex,
userInterfaceStyle: "dark",
...Style.actionSheet,
},
async (selectedIndex) => {
if (selectedIndex !== cancelButtonIndex) {
switch (selectedIndex) {
case 0:
onChooseLibrary();
break;
case 1:
onChooseDocumentPicker();
break;
case 2:
onTakePicture();
break;
default:
break;
}
}
}
);
} else {
onChooseDocumentPicker();
}
} catch (error) {
console.log(error);
setTooltip({ text: error.message, type: "error" });
} finally {
setIsLoading(false);
}
};
const onChooseLibrary = async ({} = {}) => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsEditing: false,
quality: 3,
});
if (result?.assets?.[0]?.uri) {
const { uri, fileName = "" } = result?.assets?.[0];
onUploadDocument({
files: [
{
name: getAssetName({ fileName, uri }),
uri,
type: "IMAGE",
},
],
});
} else {
throw new Error("Aucune image sélectionnée");
}
};
const onChooseDocumentPicker = async ({} = {}) => {
const result = await DocumentPicker.getDocumentAsync({
type: "*/*",
copyToCacheDirectory: false,
});
if (result?.assets?.length > 0) {
const { name = "", uri = null } = result?.assets[0] || {};
if (!uri) {
throw new Error("Erreur lors de l'ajout du document");
}
onUploadDocument({
files: [
{
name: name || getDefaultFileName(),
uri,
},
],
});
} else {
console.log(result);
throw new Error("Erreur lors de l'ajout du document");
}
};
const onTakePicture = useCallback(async () => {
const { status } = await ImagePicker.requestCameraPermissionsAsync();
if (status !== "granted") {
throw new Error("Permissions caméra non accordées");
}
const result = await ImagePicker.launchCameraAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsEditing: true,
quality: 1,
});
if (result?.assets?.[0]?.uri) {
const { uri, fileName = "" } = result?.assets?.[0];
onUploadDocument({
files: [
{
name: getAssetName({ fileName, uri }),
uri,
type: "IMAGE",
},
],
});
} else {
throw new Error("Aucune image sélectionnée");
}
}, []);
const getDefaultFileName = () => {
const randomID = Math.random().toString(36).substring(7);
return `${moment().format(`DD_MM_YYYY_HH_mm_ss`)}_${randomID}`;
};
const getAssetName = ({ fileName = "", uri = "" }) => {
let name = fileName;
if (!name && uri?.startsWith("file://")) {
const splittedArray = uri?.split("/") || [];
name = splittedArray?.[splittedArray?.length - 1] || "";
}
if (!name?.length) {
let extension = uri?.split(";")?.[0]?.split("/")?.[1] || "";
const defaultFileName = getDefaultFileName();
if (extension?.length) {
name = `${defaultFileName}.${extension}`;
} else {
name = `${defaultFileName}`;
}
}
return name;
};
const onUploadDocument = async ({ files = [] } = {}) => {
try {
setIsLoading(true);
const filesToUpdate = [];
await Promise.all(
files.map(async (file) => {
const { name = "", uri = null } = file;
console.log(file);
if (!uri) {
throw new Error("Erreur lors de l'ajout du document");
}
const fileName = name || getDefaultFileName();
let type = "FILE";
if (name?.toLowerCase()?.match(/\.(jpeg|jpg|gif|png)$/) != null) {
type = "IMAGE";
}
if (name?.toLowerCase()?.match(/\.(mp4|mov|avi|mkv)$/) != null) {
type = "VIDEO";
}
let compressedURI = uri;
let thumbnailURI = null;
if (isNative) {
if (type === "IMAGE") {
compressedURI = await Compressor.Image.compress(uri, {
compressionMethod: "manual",
maxWidth: 1000,
quality: 0.8,
});
} else if (type === "VIDEO") {
compressedURI = await Compressor.Video.compress(uri);
}
}
const { resultURI = null } = await uploadFileToFirebase({
uri: compressedURI,
path: `documents/${documentID}/files/${fileName}`,
});
if (resultURI) {
if (shouldReturnObject) {
filesToUpdate.push({
name: fileName,
uri: resultURI,
type,
thumbnailURI,
});
} else {
filesToUpdate.push(resultURI);
}
} else {
console.log("resultURI is null");
}
})
);
if (filesToUpdate.length > 0) {
if (documentExists) {
await collectionRef.doc(documentID).update({
files: arrayUnion(...filesToUpdate),
});
}
if (setFiles) {
setFiles((prev) => [...prev, ...filesToUpdate]);
}
}
setTooltip({ text: "Document(s) ajouté(s) avec succès" });
} catch (error) {
console.log(error);
setTooltip({ text: error.message, type: "error" });
} finally {
setIsLoading(false);
}
};
if (!documentID) {
return null;
}
return (
<View ref={dropRef}>
<Pressable onPress={onAddFile}>
{customElement?.() || (
<View
style={{
...Style.containerCenter,
backgroundColor: isDragging
? Palette.transparentGreen
: Palette.transparentPrimary,
borderRadius: mainBorderRadius,
borderStyle: "dashed",
borderWidth: 2,
borderColor: isDragging ? Palette.green : Palette.primary,
...containerStyle,
}}
>
<Image
source={icons.screenshot}
style={{
...Style.iconLarge,
marginBottom: gutterConstant,
}}
/>
<Text
style={{
...Fonts({ type: "default" }),
textAlign: "center",
color: Palette.white,
}}
>
{isWeb && isDesktop
? `Glissez-déposez ici\nles fichiers, images et vidéos\nà ajouter.`
: "Ajouter une image,\nun fichier ou une vidéo."}
</Text>
</View>
)}
</Pressable>
</View>
);
};
export default DocumentDropZone;
+40
View File
@@ -0,0 +1,40 @@
import React, { useState, useEffect } from "react";
import { View } from "react-native";
import { Image } from "expo-image";
const DynamicImage = ({ uri, children, ...props }) => {
const [loading, setLoading] = useState(true);
const [numRetries, setNumRetries] = useState(0);
useEffect(() => {
if (numRetries < 15) {
const timer = setTimeout(() => {
setLoading(true);
}, 2500);
return () => clearTimeout(timer);
}
}, [numRetries]);
const handleError = () => {
setLoading(false);
setNumRetries(numRetries + 1);
};
return (
<View style={{ flex: 1 }}>
{loading && numRetries < 15 && uri ? (
<Image
source={uri}
contentFit="cover"
transition={500}
{...props}
onError={handleError}
/>
) : (
<View {...props}>{children}</View>
)}
</View>
);
};
export default DynamicImage;
@@ -0,0 +1,60 @@
import React, { useGlobal } from "reactn";
import { View, Text } from "react-native";
import useLayoutType from "../hooks/useLayoutType";
import Button from "./Button";
import { Style, Fonts } from "../styles";
import { responsiveScreenHeight } from "react-native-responsive-dimensions";
const EmptyFlashListPlaceholder = ({
loading = false,
text = "-",
buttonData = {},
}) => {
const [, setShowSearch] = useGlobal("showSearch");
const { isMobile } = useLayoutType;
if (loading) {
return (
<Text style={Fonts({ type: "default", style: { textAlign: "center" } })}>
Chargement en cours...
</Text>
);
}
return (
<View
style={{
...Style.containerCenter,
alignSelf: "center",
width: isMobile ? "100%" : "50%",
height: responsiveScreenHeight(50),
}}
>
<Text style={Fonts({ type: "default", style: { textAlign: "center" } })}>
{text}
</Text>
{buttonData?.text?.length > 0 && (
<Button
text={buttonData?.text || "Ajouter une tâche"}
type="secondary"
onPress={() => {
setShowSearch(false);
buttonData?.onPress?.() || (() => console.log("null"));
}}
containerStyle={{
alignSelf: "center",
width: "100%",
}}
/>
)}
</View>
);
};
export default EmptyFlashListPlaceholder;
+134
View File
@@ -0,0 +1,134 @@
import { useContext, useGlobal } from "reactn";
import { ScrollView, Text, View, Image, Pressable } from "react-native";
import firebase, { arrayRemove } from "../config/firebase";
import { getFileNameFromURL } from "../helpers";
import { Fonts, Palette, Style, gutters } from "../styles";
import { icons } from "../assets";
import { WebViewContext } from "../providers/WebViewProvider";
import alert from "./Alert";
export default ({
files = [],
setFiles = null,
documentID = null,
collectionRef = null,
containerStyle = {},
}) => {
const [, setIsLoading] = useGlobal("_isLoading");
const [, setTooltip] = useGlobal("_tooltip");
const { setWebViewUrl } = useContext(WebViewContext);
const onDeleteFile = async (url) => {
alert(
"Êtes-vous sûr ?",
"Cette action est irréversible.",
[
{
text: "Annuler",
style: "cancel",
},
{
text: "Confirmer",
onPress: async () => {
try {
setIsLoading(true);
setFiles(
files.filter((file) => file !== url && file.uri !== url)
);
await firebase.storage().refFromURL(url).delete();
if (documentID) {
await collectionRef.doc(documentID).update({
files: arrayRemove(url),
});
}
setTooltip({
type: "success",
text: "Fichier supprimé avec succès !",
});
} catch (error) {
console.log(error);
} finally {
setIsLoading(false);
}
},
style: "confirm",
},
],
{ cancelable: false }
);
};
if (files.length === 0) {
return null;
}
return (
<View>
<ScrollView
horizontal
style={{ ...containerStyle }}
showsHorizontalScrollIndicator={false}
>
{files.map((file, index) => {
const uri = file.uri || file;
return (
<View
key={index}
style={[
Style.containerItem,
Style.containerRow,
{
backgroundColor: Palette.transparentPrimary,
paddingVertical: gutters / 4,
paddingHorizontal: 20,
marginRight: 10,
maxWidth: files?.length > 1 ? 250 : 400,
},
]}
>
<Text
numberOfLines={1}
style={Fonts({
type: "default",
color: Palette.white,
style: {
maxWidth: files?.length > 1 ? 100 : 200,
marginRight: 10,
},
})}
>
{getFileNameFromURL({ url: uri })}
</Text>
{setFiles && (
<Pressable onPress={() => onDeleteFile(uri)}>
<Image
source={icons.trash}
style={[Style.iconDefault, { marginRight: 5 }]}
resizeMode="contain"
/>
</Pressable>
)}
<Pressable onPress={() => setWebViewUrl(uri)}>
<Image
source={icons.eye}
style={Style.iconDefault}
resizeMode="contain"
/>
</Pressable>
</View>
);
})}
</ScrollView>
</View>
);
};
@@ -0,0 +1,127 @@
import { AnimatableNumericValue, StyleSheet, View } from "react-native";
import { LinearGradientProps } from "../../LinearGradient/LinearGradient";
import MaskedView from "../../MaskedView/MaskedView";
import LinearGradient from "react-native-linear-gradient";
export type GradientProps = Omit<
LinearGradientProps,
"style" | "pointerEvents">;
export type RequiredGradientBorderProps = {
/**
* Props to be passed to the gradient component. See `react-native-linear-gradient` for full list. Requires "colors" prop.
*/
gradientProps: GradientProps;
};
type BorderProps = {
/**
* Width of border.
*/
borderWidth?: number;
/**
* Border applied to top of view, overrides borderWidth.
*/
borderTopWidth?: number;
/**
* Border applied to left side of view, overrides borderWidth.
*/
borderLeftWidth?: number;
/**
* Border applied to bottom side of view, overrides borderWidth.
*/
borderBottomWidth?: number;
/**
* Border appled to right side of view, overrides borderWidth.
*/
borderRightWidth?: number;
/**
* Border radius applied to each corner.
*/
borderRadius?: AnimatableNumericValue | string | undefined;
/**
* Border radius applied to top right corner, Overrides borderRadius.
*/
borderTopRightRadius?: AnimatableNumericValue | string | undefined;
/**
* Border radius applied to top left corner. Overrides borderRadius
*/
borderTopLeftRadius?: AnimatableNumericValue | string | undefined;
/**
* Border radius applied to bottom right corner. Overrides borderRadius
*/
borderBottomRightRadius?: AnimatableNumericValue | string | undefined;
/**
* Border radius applied to bottom left corner. Overrides borderRadius
*/
borderBottomLeftRadius?: AnimatableNumericValue | string | undefined;
};
/**
* A component that applies a gradient border to the parent.
* Should be placed as the last child of the parent so that it isn't overlapped.
* @example
* ```tsx
* <View>
* <GradientBorder
* borderWidth={2}
* gradientProps={{
* colors: ['red', 'blue']
* }}
* />
* </View>
* ```
*/
export default function GradientBorder({
gradientProps,
borderWidth,
borderRadius,
borderTopRightRadius,
borderTopLeftRadius,
borderBottomLeftRadius,
borderBottomRightRadius,
borderTopWidth,
borderLeftWidth,
borderRightWidth,
borderBottomWidth
}: RequiredGradientBorderProps & BorderProps) {
return (
<MaskedView
maskElement={
<View
pointerEvents="none"
style={[
{
borderWidth,
borderRadius,
borderTopLeftRadius,
borderTopRightRadius,
borderBottomLeftRadius,
borderBottomRightRadius,
borderTopWidth,
borderLeftWidth,
borderRightWidth,
borderBottomWidth
},
StyleSheet.absoluteFill]
}
collapsable={false} />
}
style={[StyleSheet.absoluteFill]}
pointerEvents="none">
<LinearGradient
style={StyleSheet.absoluteFill}
pointerEvents="none"
{...gradientProps} />
</MaskedView>);
}
@@ -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 };
+40
View File
@@ -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;
+24
View File
@@ -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;
+31
View File
@@ -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;
+106
View File
@@ -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;
+272
View File
@@ -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 };
+116
View File
@@ -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>
</>
);
};
+49
View File
@@ -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;
+4
View File
@@ -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;
+44
View File
@@ -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;
+64
View File
@@ -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>
);
};
+58
View File
@@ -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;
+77
View File
@@ -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;
+40
View File
@@ -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>
);
};
+103
View File
@@ -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>
);
}
);
};
+30
View File
@@ -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;
+80
View File
@@ -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>
);
};
+98
View File
@@ -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;
+54
View File
@@ -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;
+96
View File
@@ -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;
+16
View File
@@ -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}
/>
);
};
+50
View File
@@ -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>
);
};
+179
View File
@@ -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;
+37
View File
@@ -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>
);
};
+3
View File
@@ -0,0 +1,3 @@
import { WebView as NativeWebView } from "react-native-webview";
export const WebView = NativeWebView;
+39
View File
@@ -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}
/>
);
};