فهرست منبع

feat: components added | rework of existing

Oleksandr Honcharov 1 سال پیش
والد
کامیت
de80cc41ae

+ 63 - 0
src/components/AvatarPicker/index.tsx

@@ -0,0 +1,63 @@
+import React, { FC, useState } from 'react';
+import { Image, Text, TouchableOpacity } from 'react-native';
+import * as ImagePicker from 'expo-image-picker';
+import * as FileSystem from 'expo-file-system';
+import AddIcon from '../../../assets/icons/add.svg';
+
+import { styles } from './styles';
+
+//TODO: simple refactor + download photo
+
+export const AvatarPicker: FC = () => {
+  const [image, setImage] = useState('');
+
+  const pickImage = async () => {
+    // No permissions request is necessary for launching the image library
+    let result = await ImagePicker.launchImageLibraryAsync({
+      mediaTypes: ImagePicker.MediaTypeOptions.Images,
+      allowsEditing: true,
+      aspect: [4, 3],
+      quality: 1
+    });
+
+    console.log(result);
+
+    if (!result.canceled) {
+      setImage(result.assets[0].uri);
+
+      // const uploadResult = await FileSystem.uploadAsync(
+      //   'https://nomadmania.eu/webapi/user/join2',
+      //   result.assets[0].uri,
+      //   {
+      //     httpMethod: 'POST',
+      //     uploadType: FileSystem.FileSystemUploadType.MULTIPART,
+      //     fieldName: 'demo_image',
+      //     headers: {
+      //       nmusername: 'test',
+      //       nmemail: 'test@gmail.com',
+      //       nmpassword: 'testpass123',
+      //       nmfirstname: 'Name',
+      //       nmlastname: 'Lastname',
+      //       nmdateofbirth: `${new Date()}`,
+      //       nmhomebase: 'testid10'
+      //     }
+      //   }
+      // );
+      //
+      // console.log(JSON.stringify(uploadResult));
+    }
+  };
+  return (
+    <TouchableOpacity style={styles.avatarWrapper} onPress={pickImage}>
+      {!image && (
+        <>
+          <AddIcon />
+          <Text style={styles.textAvatar}>Upload Photo</Text>
+        </>
+      )}
+      {image && (
+        <Image source={{ uri: image }} style={{ width: 100, height: 100, borderRadius: 100 / 2 }} />
+      )}
+    </TouchableOpacity>
+  );
+};

+ 22 - 0
src/components/AvatarPicker/styles.ts

@@ -0,0 +1,22 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from '../../theme';
+
+export const styles = StyleSheet.create({
+  textAvatar: {
+    color: Colors.DARK_BLUE,
+    fontSize: 12,
+    fontFamily: 'redhat-700'
+  },
+  avatarWrapper: {
+    width: 100,
+    height: 100,
+    borderRadius: 100 / 2,
+    borderWidth: 0.5,
+    borderColor: Colors.WHITE,
+    backgroundColor: Colors.WHITE,
+    borderStyle: 'solid',
+    display: 'flex',
+    justifyContent: 'center',
+    alignItems: 'center'
+  }
+});

+ 45 - 0
src/components/Calendar/InputDatePicker/index.tsx

@@ -0,0 +1,45 @@
+import React, { FC, useState } from 'react';
+import { View } from 'react-native';
+
+import { Input } from '../../Input';
+import { Modal } from '../../Modal';
+import SpinnerDatePicker from '../SpinnerDatePicker';
+import { Button } from '../../Button';
+
+type Props = {
+  selectedDate?: (date: Date) => void;
+};
+
+//TODO: waiting for redesign in figma + input fixes
+
+export const InputDatePicker: FC<Props> = ({ selectedDate }) => {
+  const [visible, setVisible] = useState(false);
+  const [spinnerSelectedDate, setSpinnerSelectedDate] = useState<Date>(new Date());
+
+  const onButtonPress = () => {
+    setVisible(false);
+    if (selectedDate) {
+      selectedDate(spinnerSelectedDate);
+    }
+  };
+
+  return (
+    <View>
+      <Input
+        header={'Date of birth'}
+        value={new Date(spinnerSelectedDate).toLocaleDateString()}
+        isFocused={(b) => setVisible(b)}
+        inputMode={'none'}
+      />
+      <Modal
+        visibleInPercent={'50%'}
+        onRequestClose={() => setVisible(false)}
+        headerTitle={'Select Region of Origin'}
+        visible={visible}
+      >
+        <SpinnerDatePicker selectedDate={(date) => setSpinnerSelectedDate(date)} />
+        <Button onPress={onButtonPress}>Done</Button>
+      </Modal>
+    </View>
+  );
+};

+ 30 - 0
src/components/Calendar/SpinnerDatePicker.tsx

@@ -0,0 +1,30 @@
+import React, { FC, useState } from 'react';
+import { Colors } from '../../theme';
+import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/datetimepicker';
+
+type Props = {
+  selectedDate: (date: Date) => void;
+};
+
+const SpinnerDatePicker: FC<Props> = ({ selectedDate }) => {
+  const [value, setValue] = useState<Date>(new Date());
+
+  const onChange = (event: DateTimePickerEvent, selectedSpinnerDate?: Date) => {
+    if (event.type === 'set') {
+      selectedDate(selectedSpinnerDate!);
+      setValue(selectedSpinnerDate!);
+    }
+  };
+
+  return (
+    <DateTimePicker
+      value={value}
+      textColor={Colors.DARK_BLUE}
+      mode={'date'}
+      display={'spinner'}
+      onChange={onChange}
+    />
+  );
+};
+
+export default SpinnerDatePicker;

+ 90 - 0
src/components/FlatList/index.tsx

@@ -0,0 +1,90 @@
+import React, { FC, useEffect, useState } from 'react';
+import { FlatList as List, SafeAreaView } from 'react-native';
+import { Input } from '../Input';
+import { styles } from './styles';
+import { Item, ItemData } from './item';
+
+const DATA: ItemData[] = [
+  {
+    id: 'bd7acbea-c1b1-46c2-aed5-3ad53abb28ba',
+    title: 'First Item'
+  },
+  {
+    id: '3ac68afc-c605-48d3-a4f8-fbd91aa97f63',
+    title: 'Second Item'
+  },
+  {
+    id: '58694a0f-3da1-471f-bd96-145571e29d72',
+    title: 'Third Item'
+  }
+];
+
+type Props = {
+  itemObject: (object: any) => void;
+};
+
+//TODO: rework to generic types + custom props + fetch data
+
+export const FlatList: FC<Props> = ({ itemObject }) => {
+  const [selectedObject, setSelectedObject] = useState<{ id: string; title: string }>();
+  const [search, setSearch] = useState('');
+  const [filteredData, setFilteredData] = useState<ItemData[]>([]);
+  const [masterData, setMasterData] = useState<ItemData[]>([]);
+
+  useEffect(() => {
+    setFilteredData(DATA);
+    setMasterData(DATA);
+  }, []);
+
+  const selectItem = (object: { title: string; id: string }) => {
+    itemObject(object);
+    setSelectedObject(object);
+  };
+
+  const searchFilter = (text: string) => {
+    if (text) {
+      const newData = masterData.filter((item) => {
+        const itemData = item.title ? item.title.toLowerCase() : ''.toLowerCase();
+        const textData = text.toLowerCase();
+        return itemData.indexOf(textData) > -1;
+      });
+      setFilteredData(newData);
+      setSearch(text);
+    } else {
+      setFilteredData(masterData);
+      setSearch(text);
+    }
+  };
+
+  const renderItem = ({ item }: { item: ItemData }) => {
+    const selected = item.id === selectedObject?.id;
+
+    const backgroundColor = selected ? '#FAFAFA' : 'white';
+
+    return (
+      <Item
+        selected={selected}
+        item={item}
+        onPress={() => selectItem(item)}
+        backgroundColor={backgroundColor}
+      />
+    );
+  };
+
+  return (
+    <SafeAreaView style={styles.container}>
+      <Input
+        inputMode={'search'}
+        placeholder={'test'}
+        onChange={(text) => searchFilter(text)}
+        value={search}
+      />
+      <List
+        data={filteredData}
+        renderItem={renderItem}
+        keyExtractor={(item) => item.id}
+        extraData={selectedObject}
+      />
+    </SafeAreaView>
+  );
+};

+ 40 - 0
src/components/FlatList/item.tsx

@@ -0,0 +1,40 @@
+import React from 'react';
+import { Image, Text, TouchableOpacity, View } from 'react-native';
+import { Colors } from '../../theme';
+import { styles } from './styles';
+
+import MarkSVG from '../../../assets/icons/mark.svg';
+
+//TODO: waiting for API images + split name for title and description of region
+
+export const Item = ({ item, onPress, backgroundColor, selected }: ItemProps) => (
+  <TouchableOpacity onPress={onPress} style={[styles.item, { backgroundColor }]}>
+    <View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 10 }}>
+      <Image
+        width={48}
+        height={48}
+        style={{ borderRadius: 48 / 2 }}
+        source={{
+          uri: 'file:///var/mobile/Containers/Data/Application/B7AB06B4-BE07-4161-A992-EFE45C948D82/Library/Caches/ExponentExperienceData/%2540anonymous%252Fnomadmania-app-3e93ece1-6230-4d9d-aad1-f0bff67932b5/ImagePicker/2ADA6086-B98C-4260-B2C2-B376DE410785.jpg'
+        }}
+      />
+      <View>
+        <Text style={[styles.title, { color: Colors.DARK_BLUE }]}>{item.title}</Text>
+        <Text style={[styles.text, { color: Colors.DARK_BLUE }]}>{item.title}</Text>
+      </View>
+    </View>
+    <View style={{ marginRight: 10 }}>{selected && <MarkSVG />}</View>
+  </TouchableOpacity>
+);
+
+export type ItemData = {
+  id: string;
+  title: string;
+};
+
+type ItemProps = {
+  item: ItemData;
+  onPress: () => void;
+  backgroundColor: string;
+  selected: boolean;
+};

+ 44 - 0
src/components/FlatList/modal-flatlist/index.tsx

@@ -0,0 +1,44 @@
+import React, { FC, useState } from 'react';
+import { View } from 'react-native';
+
+import { Input } from '../../Input';
+import { Modal } from '../../Modal';
+import { FlatList } from '../index';
+
+type Props = {
+  selectedObject?: (object: any) => void;
+  headerTitle: string;
+};
+
+//TODO: rework to generic types
+
+export const ModalFlatList: FC<Props> = ({ selectedObject, headerTitle }) => {
+  const [visible, setVisible] = useState(false);
+  const [selectedFlatListObject, setSelectedFlatListObject] = useState<{ title?: string }>({});
+
+  const onSelect = (object: { title: string }) => {
+    setVisible(false);
+    if (selectedObject) {
+      selectedObject(object);
+      setSelectedFlatListObject(object);
+    }
+  };
+
+  return (
+    <View>
+      <Input
+        header={headerTitle}
+        isFocused={(b) => setVisible(b)}
+        value={selectedFlatListObject?.title}
+        inputMode={'none'}
+      />
+      <Modal
+        onRequestClose={() => setVisible(false)}
+        headerTitle={'Select Region of Origin'}
+        visible={visible}
+      >
+        <FlatList itemObject={(object) => onSelect(object)} />
+      </Modal>
+    </View>
+  );
+};

+ 24 - 0
src/components/FlatList/styles.ts

@@ -0,0 +1,24 @@
+import { StatusBar, StyleSheet } from 'react-native';
+import { getFontSize } from '../../utils';
+
+export const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    marginTop: StatusBar.currentHeight || 0
+  },
+  item: {
+    width: '100%',
+    height: 60,
+    display: 'flex',
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between'
+  },
+  title: {
+    fontSize: getFontSize(14),
+    fontFamily: 'redhat-700'
+  },
+  text: {
+    fontSize: getFontSize(12)
+  }
+});

+ 20 - 4
src/components/Input/index.tsx

@@ -3,14 +3,24 @@ import { TextInput, Text, View, InputModeOptions } from 'react-native';
 import { styling } from './style';
 
 type Props = {
-  placeholder: string;
-  onChange: (text: string) => void;
+  placeholder?: string;
+  onChange?: (text: string) => void;
   header?: string;
   isPrivate?: boolean;
   inputMode?: InputModeOptions;
+  isFocused?: (boolean: boolean) => void;
+  value?: string;
 };
 
-export const Input: FC<Props> = ({ onChange, placeholder, header, isPrivate, inputMode }) => {
+export const Input: FC<Props> = ({
+  onChange,
+  placeholder,
+  header,
+  isPrivate,
+  inputMode,
+  isFocused,
+  value
+}) => {
   const [focused, setFocused] = useState(false);
 
   const styles = styling(focused);
@@ -19,11 +29,17 @@ export const Input: FC<Props> = ({ onChange, placeholder, header, isPrivate, inp
     <View>
       {header ? <Text style={styles.text}>{header}</Text> : null}
       <TextInput
+        value={value}
         inputMode={inputMode ?? 'text'}
         secureTextEntry={isPrivate ?? false}
         placeholder={placeholder}
         onChangeText={onChange}
-        onFocus={() => setFocused(true)}
+        onFocus={() => {
+          setFocused(true);
+          if (isFocused) {
+            isFocused(true);
+          }
+        }}
         onBlur={() => setFocused(false)}
         style={styles.wrapper}
       />

+ 10 - 3
src/components/Modal/ModalHeader/modal-header.tsx

@@ -1,5 +1,5 @@
 import React, { FC } from 'react';
-import { View, Text } from 'react-native';
+import { View, Text, TouchableOpacity } from 'react-native';
 
 import { styles } from './style';
 
@@ -7,12 +7,19 @@ import CloseSVG from '../../../../assets/icons/close.svg';
 
 type Props = {
   textHeader: string;
+  onRequestClose?: () => void;
 };
 
-export const ModalHeader: FC<Props> = ({ textHeader }) => {
+//TODO
+
+export const ModalHeader: FC<Props> = ({ textHeader, onRequestClose }) => {
   return (
-    <View>
+    <View style={styles.wrapperHeader}>
+      <TouchableOpacity onPress={onRequestClose} style={{ height: 30, width: 30 }}>
+        <CloseSVG />
+      </TouchableOpacity>
       <Text style={styles.textHeader}>{textHeader}</Text>
+      <Text>Temp</Text>
     </View>
   );
 };

+ 10 - 1
src/components/Modal/ModalHeader/style.ts

@@ -1,10 +1,19 @@
 import { StyleSheet } from 'react-native';
 import { Colors } from '../../../theme';
+import { getFontSize } from '../../../utils';
 
 export const styles = StyleSheet.create({
   textHeader: {
     fontFamily: 'redhat-600',
-    fontSize: 14,
+    fontSize: getFontSize(14),
     color: Colors.DARK_BLUE
+  },
+  wrapperHeader: {
+    width: '100%',
+    height: 30,
+    display: 'flex',
+    justifyContent: 'space-between',
+    flexDirection: 'row',
+    marginTop: 15
   }
 });

+ 31 - 3
src/components/Modal/index.tsx

@@ -1,13 +1,23 @@
 import React, { FC, ReactNode } from 'react';
-import { Modal as ReactModal } from 'react-native';
+import { DimensionValue, Modal as ReactModal, View } from 'react-native';
+import { ModalHeader } from './ModalHeader/modal-header';
+import { styles } from './style';
 
 type Props = {
   children: ReactNode;
   visible: boolean;
   onRequestClose?: () => void;
+  visibleInPercent?: DimensionValue;
+  headerTitle: string;
 };
 
-export const Modal: FC<Props> = ({ children, onRequestClose, visible }) => {
+export const Modal: FC<Props> = ({
+  children,
+  onRequestClose,
+  visible,
+  visibleInPercent,
+  headerTitle
+}) => {
   return (
     <ReactModal
       animationType={'slide'}
@@ -15,8 +25,26 @@ export const Modal: FC<Props> = ({ children, onRequestClose, visible }) => {
       presentationStyle={'pageSheet'}
       visible={visible}
       onRequestClose={onRequestClose}
+      transparent={true}
     >
-      {children}
+      <View
+        style={[
+          styles.wrapper,
+          {
+            height: visibleInPercent ?? '100%',
+            borderTopLeftRadius: visibleInPercent ? 10 : 0,
+            borderTopRightRadius: visibleInPercent ? 10 : 0
+          }
+        ]}
+      >
+        <ModalHeader onRequestClose={onRequestClose} textHeader={headerTitle} />
+        <Drawer />
+        {children}
+      </View>
     </ReactModal>
   );
 };
+
+const Drawer = () => {
+  return <View style={styles.drawer} />;
+};

+ 16 - 0
src/components/Modal/style.ts

@@ -0,0 +1,16 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from '../../theme';
+
+export const styles = StyleSheet.create({
+  drawer: {
+    width: '100%',
+    height: 1,
+    backgroundColor: '#E5E5E5'
+  },
+  wrapper: {
+    marginTop: 'auto',
+    backgroundColor: Colors.WHITE,
+    paddingLeft: 15,
+    paddingRight: 15
+  }
+});

+ 1 - 1
src/components/PageWrapper/styles.ts

@@ -1,5 +1,5 @@
 import { StyleSheet } from 'react-native';
 
 export const styles = StyleSheet.create({
-  wrapper: { marginLeft: '5%', marginRight: '5%' }
+  wrapper: { marginLeft: '5%', marginRight: '5%', height: '100%' }
 });

+ 2 - 0
src/components/index.ts

@@ -5,3 +5,5 @@ export * from './Header';
 export * from './Input';
 export * from './Modal';
 export * from './PageWrapper';
+export * from './AvatarPicker';
+export * from './FlatList';

+ 39 - 0
src/screens/RegisterScreen/EditAccount/index.tsx

@@ -0,0 +1,39 @@
+import React from 'react';
+import { View, ScrollView } from 'react-native';
+import { AvatarPicker, BigText, Button, Header, Input, PageWrapper } from '../../../components';
+import { InputDatePicker } from '../../../components/Calendar/InputDatePicker';
+import { ModalFlatList } from '../../../components/FlatList/modal-flatlist';
+
+//TODO: connect with API + simple refactor
+
+const EditAccount = () => {
+  return (
+    <PageWrapper>
+      <ScrollView showsVerticalScrollIndicator={false}>
+        <View style={{ gap: 10 }}>
+          <Header label={'Sign Up'} />
+          <BigText>Edit account data</BigText>
+          <View style={{ display: 'flex', alignItems: 'center' }}>
+            <AvatarPicker />
+          </View>
+          <Input placeholder={'Text'} onChange={() => {}} header={'First name'} />
+          <Input placeholder={'Text'} onChange={() => {}} header={'Last name'} />
+          <InputDatePicker selectedDate={(date) => console.log(date)} />
+          <ModalFlatList
+            headerTitle={'Region of origin'}
+            selectedObject={(data) => console.log(data)}
+          />
+          <ModalFlatList
+            headerTitle={'Second region'}
+            selectedObject={(data) => console.log(data)}
+          />
+          <View style={{ marginTop: 10 }}>
+            <Button>Sign up</Button>
+          </View>
+        </View>
+      </ScrollView>
+    </PageWrapper>
+  );
+};
+
+export default EditAccount;

+ 4 - 1
src/screens/RegisterScreen/JoinUs/index.tsx

@@ -3,6 +3,7 @@ import { View } from 'react-native';
 import { NavigationProp } from '@react-navigation/native';
 
 import { PageWrapper, Header, Button, BigText, CheckBox, Input } from '../../../components';
+import { NAVIGATION_PAGES } from '../../../types';
 
 type Props = {
   navigation: NavigationProp<any>;
@@ -37,7 +38,9 @@ const JoinUsScreen: FC<Props> = ({ navigation }) => {
         />
       </View>
       <View style={{ marginTop: '15%' }}>
-        <Button>Continue</Button>
+        <Button onPress={() => navigation.navigate(NAVIGATION_PAGES.REGISTER_ACCOUNT_DATA)}>
+          Continue
+        </Button>
       </View>
     </PageWrapper>
   );