Skip to content

Commit

Permalink
Feat/map city list screen (#17)
Browse files Browse the repository at this point in the history
mapCityListScreen
  • Loading branch information
Dawqss authored Nov 16, 2024
1 parent da93626 commit a8f903f
Show file tree
Hide file tree
Showing 36 changed files with 2,305 additions and 167 deletions.
6 changes: 5 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ module.exports = {
{ extensions: ['.js', '.jsx', '.ts', '.tsx'] }
],
'class-methods-use-this': 'off',
'import/prefer-default-export': 'off'
'import/prefer-default-export': 'off',
'no-param-reassign': 'off',
'prefer-arrow-callback': 'off',
'react/prop-types': 'off',
'react/jsx-props-no-spreading': 'off'
},
globals: {
__DEV__: true
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/check-build-deploy-to-test-users.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ jobs:
rm -rf Gemfile.lock
gem install bundler -v 2.4.22
bundle install
- name: Update .netrc
run: |
touch .netrc
echo $NETRC >> .netrc
env:
NETRC: ${{ secrets.NETRC }}
- name: Install Pods
run: |
cd ios
Expand Down
13 changes: 11 additions & 2 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import { GestureHandlerRootView } from 'react-native-gesture-handler';

import React, { ReactElement } from 'react';
import { Provider } from 'react-redux';
import { store } from '@state/store';
import { MainRouterNavigation } from './app/navigation/MainRouter.tsx';

/**
* This is main app point
* r
* @constructor
*/
function App(): ReactElement {
return <MainRouterNavigation />;
return (
<GestureHandlerRootView>
<Provider store={store}>
<MainRouterNavigation />
</Provider>
</GestureHandlerRootView>
);
}

export default App;
24 changes: 15 additions & 9 deletions app/http/WeatherApi.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import weatherApiBase from './weatherApiBase.ts';
import { WeatherResponse } from './types.ts';
import weatherApiBase from '@http/weatherApiBase.ts';
import { WeatherResponse } from '@http/types.ts';

class WeatherApi {
findCitiesWeatherInRadiusByLatLng(
lat: number,
lon: number,
numberOfCities: number,
radiusInKm: number
) {
findCitiesWeatherInRadiusByLatLng({
lat,
lon,
numberOfCities,
radiusInKm
}: {
lat: number;
lon: number;
numberOfCities: number;
radiusInKm: number;
}) {
return weatherApiBase.get<WeatherResponse>('data/2.5/find', {
params: {
lat,
lon,
cnt: numberOfCities,
radius: radiusInKm,
cluster: 'yes'
cluster: 'yes',
units: 'metric'
}
});
}
Expand Down
32 changes: 21 additions & 11 deletions app/navigation/MainRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,36 @@
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createStaticNavigation } from '@react-navigation/native';
import {
createStaticNavigation,
StaticParamList
} from '@react-navigation/native';
import { MapCityListScreen } from '@screens/MapCityListScreen/MapCityListScreen.tsx';
import { WeatherDetailsScreen } from '@screens/WeatherDetailsScreen/WeatherDetailsScreen.tsx';

/**
* Add here types for the screens that are passed by navigation
*/
type RootStackParamList = {
MapCityList: {};
WeatherDetails: {};
};
import { RootStackParamList, RouteNames } from '@navigation/types.ts';

const RootStack = createNativeStackNavigator<RootStackParamList>({
screens: {
MapCityList: {
[RouteNames.MapCityList]: {
screen: MapCityListScreen,
options: {
headerShown: false
}
},
WeatherDetails: WeatherDetailsScreen
[RouteNames.WeatherDetails]: WeatherDetailsScreen
}
});

type RootStackStaticParamList = StaticParamList<typeof RootStack>;

/**
* Override static props for navigation purposes
* useNavigation use it for checking possible routes
*/
declare global {
// eslint-disable-next-line no-unused-vars
namespace ReactNavigation {
// eslint-disable-next-line no-unused-vars
interface RootParamList extends RootStackStaticParamList {}
}
}

export const MainRouterNavigation = createStaticNavigation(RootStack);
16 changes: 16 additions & 0 deletions app/navigation/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Vars are used but airbnb config doesnt respect ts - TODO: tweak/fix eslint plugins
*/
export enum RouteNames {
// eslint-disable-next-line no-unused-vars
MapCityList = 'MapCityList',
// eslint-disable-next-line no-unused-vars
WeatherDetails = 'WeatherDetails'
}
/**
* Add here types for the screens that are passed by navigation
*/
export type RootStackParamList = {
[RouteNames.MapCityList]: {};
[RouteNames.WeatherDetails]: {};
};
47 changes: 42 additions & 5 deletions app/screens/MapCityListScreen/MapCityListScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,47 @@
import React from 'react';
import { View, Text } from 'react-native';
import React, { useCallback, useEffect, useRef } from 'react';
import { MapView, Camera } from '@rnmapbox/maps';

import { useAppDispatch } from '@state/hooks.ts';
import { fetchCitiesWeatherInRadiusByLatLngThunk } from '@state/features/weatherCityList/weatherCityListThunks.ts';
import { mapCityListScreenStyles } from '@screens/MapCityListScreen/mapCityListScreenStyles.ts';
import { MapCityListBottomSheet } from '@screens/MapCityListScreen/components/MapCityListBottomSheet/MapCityListBottomSheet.tsx';
import {
FallbackBounds,
FallbackLatLng
} from '@screens/MapCityListScreen/constants.ts';

export function MapCityListScreen() {
const cameraRef = useRef<Camera>(null);
const dispatch = useAppDispatch();
const isCameraInit = useRef(false);

useEffect(function onInit() {
dispatch(
fetchCitiesWeatherInRadiusByLatLngThunk({
lat: FallbackLatLng.lat,
lon: FallbackLatLng.lng,
numberOfCities: 20,
radiusInKm: 20
})
);
}, []);

const onCameraChanged = useCallback(() => {
if (isCameraInit.current) {
return;
}

isCameraInit.current = true;
cameraRef.current?.fitBounds(FallbackBounds.ne, FallbackBounds.sw, 20, 10);
}, []);

return (
<View>
<Text>Konichiwa I am Map City List screen</Text>
</View>
<MapView
style={mapCityListScreenStyles.map}
onCameraChanged={onCameraChanged}
>
<Camera ref={cameraRef} />
<MapCityListBottomSheet />
</MapView>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { useCallback } from 'react';
import BottomSheet, { BottomSheetView } from '@gorhom/bottom-sheet';
import { FlatList } from 'react-native-gesture-handler';

import { mapCityListScreenStyles } from '@screens/MapCityListScreen/mapCityListScreenStyles.ts';
import { useAppSelector } from '@state/hooks.ts';
import { weatherCityListSelector } from '@state/features/weatherCityList/weatherCityListSelectors.ts';
import { MapCityListItem } from '@screens/MapCityListScreen/components/MapCityListBottomSheet/MapCityListItem/MapCityListItem.tsx';
import type { ListRenderItem } from '@react-native/virtualized-lists';
import { WeatherItem } from '@http/types.ts';

export function MapCityListBottomSheet() {
const cities = useAppSelector(weatherCityListSelector);

const renderItem: ListRenderItem<WeatherItem> = useCallback(
({ ...info }) => <MapCityListItem {...info} />,
[]
);

return (
<BottomSheet snapPoints={['20%', '50%', '90%']}>
<BottomSheetView style={mapCityListScreenStyles.contentContainer}>
<FlatList data={cities} renderItem={renderItem} />
</BottomSheetView>
</BottomSheet>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, { useCallback } from 'react';
import { Image, Text, View, ListRenderItemInfo } from 'react-native';
import { TouchableOpacity } from 'react-native-gesture-handler';

import { WeatherItem } from '@http/types.ts';
import { mapCityListItemStyles } from '@screens/MapCityListScreen/components/MapCityListBottomSheet/MapCityListItem/mapCityListItemStyles.ts';
import { getWeatherIconUri } from '@screens/MapCityListScreen/components/MapCityListBottomSheet/MapCityListItem/utils/getWeatherIconUri.ts';
import { useNavigation } from '@react-navigation/native';
import { RouteNames } from '@navigation/types.ts';

export function MapCityListItem({
item
}: ListRenderItemInfo<WeatherItem>): React.ReactElement {
const { navigate } = useNavigation();
const {
name,
main: { temp },
weather: [{ main: weatherType, icon }]
} = item;

const source = { uri: getWeatherIconUri(icon) };

const onItemPress = useCallback(() => {
navigate(RouteNames.WeatherDetails);
}, []);

return (
<TouchableOpacity
style={mapCityListItemStyles.container}
onPress={onItemPress}
>
<Image
source={source}
resizeMode="cover"
style={mapCityListItemStyles.image}
/>
<View>
<Text style={mapCityListItemStyles.name}>{name}</Text>
<Text style={mapCityListItemStyles.weatherType}>{weatherType}</Text>
</View>
<View style={mapCityListItemStyles.tempContainer}>
<Text style={mapCityListItemStyles.temp}>{temp}°C</Text>
</View>
</TouchableOpacity>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { StyleSheet } from 'react-native';

export const mapCityListItemStyles = StyleSheet.create({
container: {
height: 60,
width: '100%',
flexDirection: 'row',
paddingBottom: 6,
borderBottomWidth: 1,
borderColor: 'grey'
},
image: { width: 60, height: 60, marginRight: 8 },
name: {
fontSize: 25
},
weatherType: {
fontSize: 18
},
tempContainer: {
backgroundColor: 'lightblue',
height: 40,
borderRadius: 20,
paddingHorizontal: 12,
marginLeft: 'auto',
marginRight: 12,
justifyContent: 'center',
alignSelf: 'center'
},
temp: {
fontSize: 20,
color: 'white',
fontWeight: 'bold',
textAlign: 'center'
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* return full uri of weatherApi icon
* @param iconName - name of icon from weatherApi
*/
export const getWeatherIconUri = (iconName: string) =>
`https://openweathermap.org/img/w/${iconName}.png`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { StyleSheet } from 'react-native';

export const mapCityListBottomSheetStyles = StyleSheet.create({
container: {
flex: 1
}
});
27 changes: 27 additions & 0 deletions app/screens/MapCityListScreen/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { getCenter } from 'geolib';

import { latLngTupleToObject } from '@utils/latLngTupleToObject.ts';

// [LNG, LAT]
export const FallbackBounds: {
ne: [number, number];
sw: [number, number];
} = {
ne: [14.156001300148233, 53.96161914863009],
sw: [22.892223645326283, 49.05142736914263]
};

const centerOfBounds = getCenter([
latLngTupleToObject(FallbackBounds.ne),
latLngTupleToObject(FallbackBounds.sw)
]);

export const FallbackLatLng = {
lng: 0,
lat: 0
};

if (centerOfBounds) {
FallbackLatLng.lng = centerOfBounds.longitude;
FallbackLatLng.lat = centerOfBounds.latitude;
}
17 changes: 17 additions & 0 deletions app/screens/MapCityListScreen/mapCityListScreenStyles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { StyleSheet } from 'react-native';

export const mapCityListScreenStyles = StyleSheet.create({
map: {
flex: 1
},
container: {
flex: 1,
backgroundColor: 'grey'
},
contentContainer: {
flex: 1,
paddingHorizontal: 4,
paddingTop: 4,
paddingBottom: 36
}
});
Loading

0 comments on commit a8f903f

Please sign in to comment.