소스 검색

feat: pages improvements | new page

Oleksandr Honcharov 1 년 전
부모
커밋
bf7eb46de9

+ 25 - 21
src/components/FlatList/item.tsx

@@ -4,32 +4,36 @@ import { Colors } from '../../theme';
 import { styles } from './styles';
 
 import MarkSVG from '../../../assets/icons/mark.svg';
+import { API_HOST } from '../../constants';
 
-//TODO: waiting for API images + split name for title and description of region
+export const Item = ({ item, onPress, backgroundColor, selected }: ItemProps) => {
+  const name = item.name.split('–');
 
-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>
+  return (
+    <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: `${API_HOST}/img/flags_new/${item.flag}`
+          }}
+        />
+        <View>
+          <Text style={[styles.title, { color: Colors.DARK_BLUE }]}>{name[0]}</Text>
+          <Text style={[styles.text, { color: Colors.DARK_BLUE }]}>{name[1]}</Text>
+        </View>
       </View>
-    </View>
-    <View style={{ marginRight: 10 }}>{selected && <MarkSVG />}</View>
-  </TouchableOpacity>
-);
+      <View style={{ marginRight: 10 }}>{selected && <MarkSVG />}</View>
+    </TouchableOpacity>
+  );
+};
 
 export type ItemData = {
-  id: string;
-  title: string;
+  id: number;
+  name: string;
+  flag: string;
 };
 
 type ItemProps = {

+ 99 - 96
src/screens/InAppScreens/MapScreen/index.tsx

@@ -1,11 +1,4 @@
-import {
-  View,
-  Platform,
-  TouchableOpacity,
-  Text,
-  Linking,
-  Animated,
-} from 'react-native';
+import { View, Platform, TouchableOpacity, Text, Linking, Animated } from 'react-native';
 import React, { useEffect, useState, useRef, useMemo } from 'react';
 import MapView, { UrlTile, Geojson, Marker } from 'react-native-maps';
 import * as turf from '@turf/turf';
@@ -22,7 +15,7 @@ import CloseSvg from '../../../../assets/icons/close.svg';
 import regions from '../../../../assets/geojson/nm2022.json';
 import dareRegions from '../../../../assets/geojson/mqp.json';
 
-import NetInfo from "@react-native-community/netinfo";
+import NetInfo from '@react-native-community/netinfo';
 import { getFirstDatabase, getSecondDatabase } from '../../../db';
 import { RegionPopup, LocationPopup } from '../../../components';
 
@@ -49,7 +42,7 @@ import {
   MapScreenProps,
   FeatureCollection
 } from '../../../types/map';
-const MAP_HOST ='https://maps.nomadmania.eu';
+const MAP_HOST = 'https://maps.nomadmania.eu';
 
 const tilesBaseURL = `${MAP_HOST}/tiles_osm`;
 const localTileDir = `${FileSystem.cacheDirectory}tiles`;
@@ -91,11 +84,11 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
 
   useEffect(() => {
     const fetchData = async () => {
-        const fetchedUserId = await storageGet('uid');
-        const fetchedToken = await storageGet('token');
+      const fetchedUserId = await storageGet('uid');
+      const fetchedToken = await storageGet('token');
 
-        setUserId(fetchedUserId);
-        setToken(fetchedToken);
+      setUserId(fetchedUserId);
+      setToken(fetchedToken);
     };
 
     fetchData();
@@ -107,13 +100,13 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
         Animated.timing(strokeWidthAnim, {
           toValue: 3,
           duration: 700,
-          useNativeDriver: false,
+          useNativeDriver: false
         }),
         Animated.timing(strokeWidthAnim, {
           toValue: 2,
           duration: 700,
-          useNativeDriver: false,
-        }),
+          useNativeDriver: false
+        })
       ])
     ).start();
   }, [strokeWidthAnim]);
@@ -125,18 +118,18 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
         position: 'absolute',
         ...Platform.select({
           android: {
-            height: 58,
-          },
-        }),
+            height: 58
+          }
+        })
       }
     });
   }, [regionPopupVisible, navigation]);
 
   useEffect(() => {
-    const unsubscribe = NetInfo.addEventListener(state => {
+    const unsubscribe = NetInfo.addEventListener((state) => {
       setIsConnected(state.isConnected);
     });
-  
+
     return () => unsubscribe();
   }, []);
 
@@ -149,17 +142,25 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
 
       let currentLocation = await Location.getCurrentPositionAsync({});
       setLocation(currentLocation.coords);
-      
-      mapRef.current?.animateToRegion({
-        latitude: currentLocation.coords.latitude,
-        longitude: currentLocation.coords.longitude,
-        latitudeDelta: 5,
-        longitudeDelta: 5,
-      }, 1000);
+
+      mapRef.current?.animateToRegion(
+        {
+          latitude: currentLocation.coords.latitude,
+          longitude: currentLocation.coords.longitude,
+          latitudeDelta: 5,
+          longitudeDelta: 5
+        },
+        1000
+      );
     })();
   }, []);
 
-  const findFeaturesInVisibleMapArea = async (visibleMapArea: { latitude?: any; longitude?: any; latitudeDelta: any; longitudeDelta?: any; }) => {
+  const findFeaturesInVisibleMapArea = async (visibleMapArea: {
+    latitude?: any;
+    longitude?: any;
+    latitudeDelta: any;
+    longitudeDelta?: any;
+  }) => {
     const currentZoom = Math.log2(360 / visibleMapArea.latitudeDelta);
 
     if (cancelTokenRef.current) {
@@ -177,21 +178,23 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
 
       return;
     }
-  
+
     const { latitude, longitude, latitudeDelta, longitudeDelta } = visibleMapArea;
     const bbox: turf.BBox = [
       longitude - longitudeDelta / 2,
       latitude - latitudeDelta / 2,
       longitude + longitudeDelta / 2,
-      latitude + latitudeDelta / 2,
+      latitude + latitudeDelta / 2
     ];
     const visibleAreaPolygon = turf.bboxPolygon(bbox);
     const regionsFound = filterCandidates(regions, bbox);
     // const daresFound = filterCandidates(dareRegions, bbox);
-  
-    const regionIds = regionsFound.map((region: { properties: { id: any; }; }) => region.properties.id);
+
+    const regionIds = regionsFound.map(
+      (region: { properties: { id: any } }) => region.properties.id
+    );
     const candidatesMarkers = await fetchSeriesData(token, JSON.stringify(regionIds));
-    
+
     if (thisToken !== currentTokenRef.current) return;
 
     setSeries(candidatesMarkers.series);
@@ -208,19 +211,20 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
 
     const singleMarkers = markers.filter((feature) => {
       return feature.properties.dbscan !== 'core';
-    })
+    });
 
     return (
       <>
         {singleMarkers.map((marker, idx) => {
-          const markerSeries = series?.find(s => s.id === marker.properties.series_id);
+          const markerSeries = series?.find((s) => s.id === marker.properties.series_id);
           const iconUrl = markerSeries ? processIconUrl(markerSeries.icon) : 'default_icon_url';
 
           return <MarkerItem marker={marker} iconUrl={iconUrl} key={idx} />;
         })}
-        {clusters && Object.entries(clusters).map(([clusterId, data], idx) => (
-          <ClusterItem clusterId={clusterId} data={data} key={idx} />
-        ))}
+        {clusters &&
+          Object.entries(clusters).map(([clusterId, data], idx) => (
+            <ClusterItem clusterId={clusterId} data={data} key={idx} />
+          ))}
       </>
     );
   };
@@ -240,20 +244,23 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
   const getLocation = async () => {
     let currentLocation = await Location.getCurrentPositionAsync();
     setLocation(currentLocation.coords);
-    
-    mapRef.current?.animateToRegion({
-      latitude: currentLocation.coords.latitude,
-      longitude: currentLocation.coords.longitude,
-      latitudeDelta: 5,
-      longitudeDelta: 5,
-    }, 800);
+
+    mapRef.current?.animateToRegion(
+      {
+        latitude: currentLocation.coords.latitude,
+        longitude: currentLocation.coords.longitude,
+        latitudeDelta: 5,
+        longitudeDelta: 5
+      },
+      800
+    );
 
     handleClosePopup();
   };
 
   const handleAcceptPermission = async () => {
     setAskLocationVisible(false);
-    let { status, canAskAgain  } = await Location.requestForegroundPermissionsAsync();
+    let { status, canAskAgain } = await Location.requestForegroundPermissionsAsync();
 
     if (status === 'granted') {
       getLocation();
@@ -267,7 +274,9 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
     setUserAvatars(avatars);
   };
 
-  const handleMapPress = async (event: { nativeEvent: { coordinate: { latitude: any; longitude: any; }; }; }) => {
+  const handleMapPress = async (event: {
+    nativeEvent: { coordinate: { latitude: any; longitude: any } };
+  }) => {
     cancelTokenRef.current = true;
     const { latitude, longitude } = event.nativeEvent.coordinate;
     const point = turf.point([longitude, latitude]);
@@ -289,31 +298,33 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
 
       setSelectedRegion({
         type: 'FeatureCollection',
-        features: [{
-          geometry: foundRegion.geometry,
-          properties: {
-            ...foundRegion.properties,
-            fill: "rgba(57, 115, 172, 0.2)",
-            stroke: "#3973AC",
-          },
-          type: 'Feature',
-        }]
+        features: [
+          {
+            geometry: foundRegion.geometry,
+            properties: {
+              ...foundRegion.properties,
+              fill: 'rgba(57, 115, 172, 0.2)',
+              stroke: '#3973AC'
+            },
+            type: 'Feature'
+          }
+        ]
       });
 
       await getData(db, id, tableName, handleRegionData)
-      .then(() => {
-        setRegionPopupVisible(true);
-      })
-      .catch(error => {
-        console.error("Error fetching data", error);
-      });
+        .then(() => {
+          setRegionPopupVisible(true);
+        })
+        .catch((error) => {
+          console.error('Error fetching data', error);
+        });
 
       const bounds = turf.bbox(foundRegion);
       const region = calculateMapRegion(bounds);
 
       mapRef.current?.animateToRegion(region, 1000);
 
-      if(tableName === 'regions') {
+      if (tableName === 'regions') {
         const seriesData = await fetchSeriesData(token, JSON.stringify([id]));
         setSeries(seriesData.series);
         const allMarkers = seriesData.items.map(processMarkerData);
@@ -326,12 +337,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
     }
   };
 
-  const renderMapTiles = (
-    url: string,
-    cacheDir: string,
-    zIndex: number,
-    opacity = 1
-  ) => (
+  const renderMapTiles = (url: string, cacheDir: string, zIndex: number, opacity = 1) => (
     <UrlTile
       urlTemplate={`${url}/{z}/{x}/{y}`}
       maximumZ={15}
@@ -349,15 +355,15 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
     if (!selectedRegion) return null;
 
     return (
-        <Geojson
-          geojson={selectedRegion as any}
-          fillColor="rgba(57, 115, 172, 0.2)"
-          strokeColor="#3973ac"
-          strokeWidth={Platform.OS == 'android' ? 3 : 2}
-          zIndex={3}
-        />
+      <Geojson
+        geojson={selectedRegion as any}
+        fillColor="rgba(57, 115, 172, 0.2)"
+        strokeColor="#3973ac"
+        strokeWidth={Platform.OS == 'android' ? 3 : 2}
+        zIndex={3}
+      />
     );
-  };
+  }
 
   const handleClosePopup = async () => {
     cancelTokenRef.current = false;
@@ -374,7 +380,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
     const longitude = (southWest?.longitude ?? 0) + longitudeDelta / 2;
 
     findFeaturesInVisibleMapArea({ latitude, longitude, latitudeDelta, longitudeDelta });
-  }
+  };
 
   const renderedGeoJSON = useMemo(() => renderGeoJSON(), [selectedRegion]);
 
@@ -398,10 +404,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
         {userId && renderMapTiles(visitedTiles, localVisitedDir, 2, 0.5)}
         {renderMapTiles(dareTiles, localDareDir, 2, 0.5)}
         {location && (
-          <AnimatedMarker
-            coordinate={location}
-            anchor={{ x: 0.5, y: 0.5 }}
-          >
+          <AnimatedMarker coordinate={location} anchor={{ x: 0.5, y: 0.5 }}>
             <Animated.View style={[styles.location, { borderWidth: strokeWidthAnim }]} />
           </AnimatedMarker>
         )}
@@ -417,35 +420,33 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
       <LocationPopup
         visible={openSettingsVisible}
         onClose={() => setOpenSettingsVisible(false)}
-        onAccept={() => Platform.OS === 'ios' ? Linking.openURL('app-settings:') : Linking.openSettings()}
+        onAccept={() =>
+          Platform.OS === 'ios' ? Linking.openURL('app-settings:') : Linking.openSettings()
+        }
         modalText="NomadMania app needs location permissions to function properly. Open settings?"
       />
 
       {regionPopupVisible && regionData ? (
         <>
           <TouchableOpacity
-            style={[ styles.cornerButton, styles.topLeftButton, styles.closeLeftButton ]}
+            style={[styles.cornerButton, styles.topLeftButton, styles.closeLeftButton]}
             onPress={handleClosePopup}
           >
-            <CloseSvg
-              fill="white"
-              width={13}
-              height={13}
-            />
+            <CloseSvg fill="white" width={13} height={13} />
             <Text style={styles.textClose}>Close</Text>
           </TouchableOpacity>
 
           <TouchableOpacity
             onPress={handleGetLocation}
-            style={[ styles.cornerButton, styles.topRightButton, styles.bottomButton ]}
+            style={[styles.cornerButton, styles.topRightButton, styles.bottomButton]}
           >
             <LocationIcon />
           </TouchableOpacity>
 
-          <RegionPopup 
+          <RegionPopup
             region={regionData}
             userAvatars={userAvatars}
-            onMarkVisited={() => console.log('Mark as visited')} 
+            onMarkVisited={() => console.log('Mark as visited')}
           />
         </>
       ) : (
@@ -458,7 +459,9 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
             <SearchIcon />
           </TouchableOpacity>
 
-          <TouchableOpacity style={[styles.cornerButton, styles.bottomButton, styles.bottomLeftButton]}>
+          <TouchableOpacity
+            style={[styles.cornerButton, styles.bottomButton, styles.bottomLeftButton]}
+          >
             <RadarIcon />
           </TouchableOpacity>
 
@@ -472,6 +475,6 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
       )}
     </View>
   );
-}
+};
 
 export default MapScreen;

+ 290 - 0
src/screens/InAppScreens/ProfileScreen/Profile/edit-personal-info.tsx

@@ -0,0 +1,290 @@
+import React, { useEffect, useState } from 'react';
+import { ScrollView, View, Text } from 'react-native';
+import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
+import { Formik } from 'formik';
+import * as yup from 'yup';
+import { useNavigation } from '@react-navigation/native';
+import { useQueryClient } from '@tanstack/react-query';
+import { Image } from 'expo-image';
+
+import { API_HOST } from '../../../../constants';
+
+import { AvatarPicker, BigText, Header, Input, PageWrapper, Button } from '../../../../components';
+import { InputDatePicker } from '../../../../components/Calendar/InputDatePicker';
+import { ModalFlatList } from '../../../../components/FlatList/modal-flatlist';
+
+import { usePostGetProfileQuery } from '../../../../modules/auth/user/queries/use-post-get-profile';
+import { useGetRegionsWithFlagQuery } from '../../../../modules/auth/regions/queries/use-post-get-regions';
+import { usePostSetProfileMutation } from '../../../../modules/auth/user/queries/use-post-set-profile';
+import { userQueryKeys } from '../../../../modules/auth/user/user-query-keys';
+
+import type { PostSetProfileData } from '../../../../modules/auth/user/user-api';
+
+import { storageGet } from '../../../../storage';
+import { Colors } from '../../../../theme';
+
+import FacebookIcon from '../../../../../assets/icons/facebook.svg';
+import InstagramIcon from '../../../../../assets/icons/instagram.svg';
+import XIcon from '../../../../../assets/icons/x(twitter).svg';
+import YoutubeIcon from '../../../../../assets/icons/youtube.svg';
+import GlobeIcon from '../../../../../assets/icons/bottom-navigation/globe.svg';
+import LinkIcon from '../../../../../assets/icons/link.svg';
+
+const ProfileSchema = yup.object({
+  username: yup.string().optional(),
+  email: yup.string().email().optional(),
+  first_name: yup.string().optional(),
+  last_name: yup.string().optional(),
+  date_of_birth: yup.string().optional(),
+  homebase: yup.number().optional(),
+  homebase2: yup.number().nullable().optional(),
+  bio: yup.string().optional(),
+  f: yup.string().optional(),
+  t: yup.string().optional(),
+  i: yup.string().optional(),
+  y: yup.string().optional(),
+  www: yup.string().optional(),
+  other: yup.string().optional()
+});
+
+export const EditPersonalInfo = () => {
+  const [token, setToken] = useState<string | null>('');
+
+  const { mutate: updateProfile, data: updateResponse, reset } = usePostSetProfileMutation();
+
+  useEffect(() => {
+    async function getToken() {
+      setToken(await storageGet('token'));
+    }
+    reset();
+
+    getToken();
+  }, []);
+
+  const navigation = useNavigation();
+  const queryClient = useQueryClient();
+
+  const { data, error } = usePostGetProfileQuery(token!, true);
+
+  const regions = useGetRegionsWithFlagQuery(true);
+
+  if (!data) return <Text>Loading</Text>;
+
+  const originRegion = regions.data?.data.find((region) => region.id === data.homebase);
+  const secondOrigin = regions.data?.data.find((region) => region.id === data.homebase2);
+
+  return (
+    <PageWrapper>
+      <ScrollView showsVerticalScrollIndicator={false}>
+        <Header label={'Edit Personal Info'} />
+        <KeyboardAwareScrollView>
+          <Formik
+            validationSchema={ProfileSchema}
+            initialValues={{
+              username: data.username,
+              email: data.email,
+              first_name: data.first_name,
+              last_name: data.last_name,
+              date_of_birth: data.date_of_birth,
+              homebase: data.homebase,
+              homebase2: data.homebase2,
+              bio: data.bio.toString(),
+              f: data.links.f!.link,
+              i: data.links.i!.link,
+              t: data.links.t!.link,
+              y: data.links.y!.link,
+              www: data.links.www!.link,
+              other: data.links.other!.link,
+              photo: {
+                type: '',
+                uri: '',
+                name: ''
+              }
+            }}
+            onSubmit={async (values) => {
+              const profileData: PostSetProfileData = {
+                token: token,
+                user: {
+                  username: values.username,
+                  email: values.email,
+                  first_name: values.first_name,
+                  last_name: values.last_name,
+                  date_of_birth: values.date_of_birth,
+                  homebase: values.homebase,
+                  bio: values.bio,
+                  f: values.f,
+                  i: values.i,
+                  t: values.t,
+                  y: values.y,
+                  www: values.www,
+                  other: values.other
+                }
+              };
+
+              if (values.homebase2) {
+                profileData.user!.homebase2 = values.homebase2;
+              }
+
+              if (values.photo.uri) {
+                profileData.photo = {
+                  type: values.photo.type,
+                  uri: values.photo.uri,
+                  name: values.photo.uri.split('/').pop()!
+                };
+              }
+
+              updateProfile(profileData, {
+                onSuccess: () => {
+                  queryClient.invalidateQueries({
+                    queryKey: userQueryKeys.getProfileData(),
+                    refetchType: 'all'
+                  });
+
+                  Image.clearDiskCache();
+                  Image.clearMemoryCache();
+
+                  navigation.goBack();
+                }
+              });
+            }}
+          >
+            {(props) => (
+              <View style={{ gap: 10 }}>
+                <View style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
+                  <AvatarPicker
+                    defaultAvatar={API_HOST + '/img/avatars/' + data.avatar}
+                    selectedAvatar={(asset) => props.setFieldValue('photo', asset)}
+                  />
+                </View>
+                <BigText>Account</BigText>
+                <Input
+                  editable={false}
+                  header={"Username (can't edit)"}
+                  placeholder={'Text'}
+                  inputMode={'text'}
+                  onChange={props.handleChange('username')}
+                  value={props.values.username}
+                  onBlur={props.handleBlur('username')}
+                  formikError={props.touched.username && props.errors.username}
+                />
+                <Input
+                  editable={false}
+                  header={"Email address (can't edit)"}
+                  placeholder={'Email'}
+                  inputMode={'email'}
+                  onChange={props.handleChange('email')}
+                  value={props.values.email}
+                  onBlur={props.handleBlur('email')}
+                  formikError={props.touched.email && props.errors.email}
+                />
+                <BigText>General Info</BigText>
+                <Input
+                  header={'First name'}
+                  placeholder={'Text'}
+                  inputMode={'text'}
+                  onChange={props.handleChange('first_name')}
+                  value={props.values.first_name}
+                  onBlur={props.handleBlur('first_name')}
+                  formikError={props.touched.first_name && props.errors.first_name}
+                />
+                <Input
+                  header={'Last name'}
+                  placeholder={'Text'}
+                  inputMode={'text'}
+                  onChange={props.handleChange('last_name')}
+                  value={props.values.last_name}
+                  onBlur={props.handleBlur('last_name')}
+                  formikError={props.touched.last_name && props.errors.last_name}
+                />
+                <InputDatePicker
+                  headerTitle={'Date of birth'}
+                  defaultDate={new Date(data.date_of_birth)}
+                  selectedDate={(date) => props.setFieldValue('date_of_birth', date)}
+                  formikError={props.touched.date_of_birth && props.errors.date_of_birth}
+                />
+                <ModalFlatList
+                  headerTitle={'Region of origin'}
+                  defaultObject={{ name: originRegion?.name }}
+                  selectedObject={(data) => props.setFieldValue('homebase', data.id)}
+                />
+                <ModalFlatList
+                  headerTitle={'Second region'}
+                  defaultObject={{ name: secondOrigin?.name }}
+                  selectedObject={(data) => props.setFieldValue('homebase2', data.id)}
+                />
+                <Input
+                  multiline={true}
+                  header={'Bio'}
+                  placeholder={'Text'}
+                  inputMode={'text'}
+                  onChange={props.handleChange('bio')}
+                  value={props.values.bio}
+                  onBlur={props.handleBlur('bio')}
+                  formikError={props.touched.bio && props.errors.bio}
+                />
+                <BigText>Links</BigText>
+                <Input
+                  icon={<FacebookIcon />}
+                  placeholder={'https://www.facebook.com'}
+                  inputMode={'text'}
+                  onChange={props.handleChange('f')}
+                  value={props.values.f as unknown as string}
+                  onBlur={props.handleBlur('f')}
+                  formikError={props.touched.f && props.errors.f}
+                />
+                <Input
+                  icon={<InstagramIcon />}
+                  placeholder={'https://www.instagram.com'}
+                  inputMode={'text'}
+                  onChange={props.handleChange('i')}
+                  value={props.values.i as unknown as string}
+                  onBlur={props.handleBlur('i')}
+                  formikError={props.touched.i && props.errors.i}
+                />
+                <Input
+                  icon={<XIcon />}
+                  placeholder={'https://www.twitter.com'}
+                  inputMode={'text'}
+                  onChange={props.handleChange('t')}
+                  value={props.values.t as unknown as string}
+                  onBlur={props.handleBlur('t')}
+                  formikError={props.touched.t && props.errors.t}
+                />
+                <Input
+                  icon={<YoutubeIcon />}
+                  placeholder={'https://www.youtube.com'}
+                  inputMode={'text'}
+                  onChange={props.handleChange('y')}
+                  value={props.values.y as unknown as string}
+                  onBlur={props.handleBlur('y')}
+                  formikError={props.touched.y && props.errors.y}
+                />
+                <Input
+                  icon={<GlobeIcon fill={Colors.LIGHT_GRAY} />}
+                  placeholder={'My Website'}
+                  inputMode={'text'}
+                  onChange={props.handleChange('www')}
+                  value={props.values.www as unknown as string}
+                  onBlur={props.handleBlur('www')}
+                  formikError={props.touched.www && props.errors.www}
+                />
+                <Input
+                  icon={<LinkIcon />}
+                  placeholder={'Other link'}
+                  inputMode={'text'}
+                  onChange={props.handleChange('other')}
+                  value={props.values.other as unknown as string}
+                  onBlur={props.handleBlur('other')}
+                  formikError={props.touched.other && props.errors.other}
+                />
+                <View style={{ marginTop: 15, marginBottom: 15 }}>
+                  <Button onPress={props.handleSubmit}>Save</Button>
+                </View>
+              </View>
+            )}
+          </Formik>
+        </KeyboardAwareScrollView>
+      </ScrollView>
+    </PageWrapper>
+  );
+};

+ 79 - 28
src/screens/InAppScreens/ProfileScreen/index.tsx

@@ -1,13 +1,26 @@
-import React, { FC, ReactNode } from 'react';
-import { Image, Text, View } from 'react-native';
+import React, { FC, ReactNode, useEffect, useState } from 'react';
+import { Text, TouchableOpacity, View } from 'react-native';
+import { Image } from 'expo-image';
 import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
+import { NavigationProp } from '@react-navigation/native';
 
 import { PageWrapper } from '../../../components';
 import { Colors } from '../../../theme';
-import { getFontSize } from '../../../utils';
 import { styles } from './styles';
+
+import { API_HOST } from '../../../constants';
+
+import { usePostGetProfileQuery } from '../../../modules/auth/user/queries/use-post-get-profile';
+
+import { NAVIGATION_PAGES } from '../../../types';
 import { navigationOpts } from './navigation-opts';
 
+import { storageGet } from '../../../storage';
+
+import PenIcon from '../../../../assets/icons/pen.svg';
+
+import { getFontSize, getYears } from '../../../utils';
+
 const regions = [
   {
     count: 28,
@@ -39,42 +52,74 @@ const Tab = createMaterialTopTabNavigator();
 
 // TODO: refactor + connect with API
 
-const ProfileScreen = () => {
+type Props = {
+  navigation: NavigationProp<any>;
+};
+
+const ProfileScreen: FC<Props> = ({ navigation }) => {
+  const [token, setToken] = useState<string>('');
+
+  const { data, error } = usePostGetProfileQuery(token!, true);
+
+  useEffect(() => {
+    async function getToken() {
+      const storageToken = await storageGet('token');
+      setToken(storageToken as unknown as string);
+    }
+
+    getToken();
+  }, []);
+
+  if (!data) return <Text>Loading</Text>;
+
   return (
     <PageWrapper>
       <View style={styles.pageWrapper}>
         <View>
           <Image
-            width={64}
-            height={64}
-            style={{ borderRadius: 64 / 2 }}
+            style={{ borderRadius: 64 / 2, width: 64, height: 64 }}
             source={{
-              uri: 'https://harrymitsidis.com/wp-content/uploads/2023/05/harrymitsidis-SaoTome.jpg'
+              uri: API_HOST + '/img/avatars/' + data.avatar
             }}
           />
         </View>
         <View>
-          <Text style={[styles.headerText, { fontSize: getFontSize(18) }]}>Harry Mitsidis</Text>
+          <Text style={[styles.headerText, { fontSize: getFontSize(18) }]}>
+            {data.first_name} {data.last_name}
+          </Text>
           <View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 10 }}>
             <Image
               source={{
-                uri: 'https://flagpedia.net/data/flags/w580/gr.webp'
+                uri: API_HOST + '/img/flags_new/' + data.homebase + '.png'
               }}
               style={styles.countryFlag}
-              width={20}
-              height={20}
             />
             <Image
               source={{
                 uri: 'https://upload.wikimedia.org/wikipedia/commons/b/b6/Flag_of_Canada.png'
               }}
               style={[styles.countryFlag, { marginLeft: -15 }]}
-              width={20}
-              height={20}
             />
-            <Text>Age: 40</Text>
+            <Text>Age: {getYears(data.date_of_birth)}</Text>
           </View>
         </View>
+        <View>
+          <TouchableOpacity
+            style={{
+              width: 40,
+              height: 40,
+              borderRadius: 40 / 2,
+              borderWidth: 1,
+              borderColor: Colors.LIGHT_GRAY,
+              display: 'flex',
+              justifyContent: 'center',
+              alignItems: 'center'
+            }}
+            onPress={() => navigation.navigate(NAVIGATION_PAGES.EDIT_PERSONAL_INFO)}
+          >
+            <PenIcon />
+          </TouchableOpacity>
+        </View>
       </View>
       <Tab.Navigator
         screenOptions={{
@@ -84,7 +129,12 @@ const ProfileScreen = () => {
           )
         }}
       >
-        <Tab.Screen name="Personal Info" component={PersonalInfo} />
+        <Tab.Screen
+          name="Personal Info"
+          component={() => (
+            <PersonalInfo data={{ bio: data.bio, date_of_birth: data.date_of_birth }} />
+          )}
+        />
         <Tab.Screen name="Visited Regions" component={() => <Text>Visited Regions</Text>} />
         <Tab.Screen name="Photos" component={() => <Text>Photos</Text>} />
       </Tab.Navigator>
@@ -92,7 +142,14 @@ const ProfileScreen = () => {
   );
 };
 
-const PersonalInfo = () => {
+type PersonalInfoProps = {
+  data: {
+    bio: string;
+    date_of_birth: string;
+  };
+};
+
+const PersonalInfo: FC<PersonalInfoProps> = ({ data }) => {
   return (
     <View style={{ marginTop: 20, gap: 20 }}>
       <InfoItem inline={true} title={'Visited Regions'}>
@@ -132,7 +189,7 @@ const PersonalInfo = () => {
             fontSize: getFontSize(14)
           }}
         >
-          Jan 01, 1980
+          {data.date_of_birth}
         </Text>
       </View>
       <View style={{ display: 'flex', flexDirection: 'row' }}>
@@ -158,30 +215,24 @@ const PersonalInfo = () => {
         </Text>
       </View>
       <InfoItem title={'Bio'}>
-        <Text>
-          Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
-          commodo consequat.
-        </Text>
+        <Text>{data.bio}</Text>
       </InfoItem>
       <InfoItem title={'Social links'}>
         <View style={{ display: 'flex', flexDirection: 'row', gap: 10 }}>
           <Image
-            width={20}
-            height={20}
+            style={{ width: 20, height: 20 }}
             source={{
               uri: 'https://upload.wikimedia.org/wikipedia/commons/6/6c/Facebook_Logo_2023.png'
             }}
           />
           <Image
-            width={20}
-            height={20}
+            style={{ width: 20, height: 20 }}
             source={{
               uri: 'https://upload.wikimedia.org/wikipedia/commons/a/a5/Instagram_icon.png'
             }}
           />
           <Image
-            width={20}
-            height={20}
+            style={{ width: 20, height: 20 }}
             source={{
               uri: 'https://w7.pngwing.com/pngs/208/269/png-transparent-youtube-play-button-computer-icons-youtube-youtube-logo-angle-rectangle-logo-thumbnail.png'
             }}

+ 2 - 0
src/screens/InAppScreens/ProfileScreen/styles.ts

@@ -16,6 +16,8 @@ export const styles = StyleSheet.create({
     fontSize: getFontSize(14)
   },
   countryFlag: {
+    width: 20,
+    height: 20,
     borderRadius: 20 / 2,
     borderWidth: 0.5,
     borderColor: 'gray'

+ 3 - 0
src/utils/responsive-font.ts

@@ -2,3 +2,6 @@ import { PixelRatio } from 'react-native';
 
 const fontScale = PixelRatio.getFontScale();
 export const getFontSize = (size: number) => size / fontScale;
+
+export const getYears = (date: string) =>
+  Math.abs(new Date(date).getFullYear() - new Date().getFullYear());