Repository: kiwicom/mobile
Pull request #1122 (2018-09-24 08:26:43)

Feature/new all hotels map
Should be ready to be merged. There might be a few details missing.

tbergq: @RobinCsl 🚀 

Patch:
diff --git a/app/hotels/src/allHotels/AllHotelsSearchList.js b/app/hotels/src/allHotels/AllHotelsSearchList.js
index 3a197a3e..c067d7f7 100644
--- a/app/hotels/src/allHotels/AllHotelsSearchList.js
+++ b/app/hotels/src/allHotels/AllHotelsSearchList.js
@@ -3,7 +3,12 @@
 import * as React from 'react';
 import idx from 'idx';
 import { ScrollView } from 'react-native';
-import { Logger, GeneralError, StyleSheet } from '@kiwicom/mobile-shared';
+import {
+  Logger,
+  GeneralError,
+  StyleSheet,
+  Device,
+} from '@kiwicom/mobile-shared';
 import {
   createPaginationContainer,
   graphql,
@@ -127,7 +132,7 @@ function AllHotelsSearchListWithContext(props: Props) {
 
 const styles = StyleSheet.create({
   content: {
-    paddingBottom: 44,
+    paddingBottom: Device.isIPhoneX ? 80 : 44,
   },
 });
 
diff --git a/app/hotels/src/allHotels/AllHotelsSearchRow.js b/app/hotels/src/allHotels/AllHotelsSearchRow.js
index aa09d803..c812d5bc 100644
--- a/app/hotels/src/allHotels/AllHotelsSearchRow.js
+++ b/app/hotels/src/allHotels/AllHotelsSearchRow.js
@@ -13,7 +13,7 @@ import {
 import { defaultTokens } from '@kiwicom/mobile-orbit';
 
 import HotelTitle from './HotelTitle';
-import HotelReviewScore from './HotelReviewScore';
+import HotelReviewScore from '../components/HotelReviewScore';
 import type { AllHotelsSearchRow as AllHotelsSearchRowProps } from './__generated__/AllHotelsSearchRow.graphql';
 import SingleHotelContext from '../navigation/singleHotel/SingleHotelContext';
 
diff --git a/app/hotels/src/allHotels/RenderSearchResults.js b/app/hotels/src/allHotels/RenderSearchResults.js
index e7291803..dff9953a 100644
--- a/app/hotels/src/allHotels/RenderSearchResults.js
+++ b/app/hotels/src/allHotels/RenderSearchResults.js
@@ -1,7 +1,7 @@
 // @flow strict
 
 import * as React from 'react';
-import { Animated, View } from 'react-native';
+import { Animated } from 'react-native';
 import { graphql, createFragmentContainer } from '@kiwicom/mobile-relay';
 import { StyleSheet, AdaptableLayout } from '@kiwicom/mobile-shared';
 
@@ -63,10 +63,11 @@ class RenderSearchResults extends React.Component<PropsWithContext> {
     }
   };
   render = () => (
-    <View style={styles.container}>
+    <React.Fragment>
       <Animated.View
         style={{
           ...StyleSheet.absoluteFillObject,
+          top: 56,
           transform: [{ translateY: this.listAnimation }],
         }}
       >
@@ -80,7 +81,7 @@ class RenderSearchResults extends React.Component<PropsWithContext> {
           <Animated.View
             style={{
               ...StyleSheet.absoluteFillObject,
-              bottom: 44,
+              top: 56,
               transform: [{ translateY: this.mapAnimation }],
             }}
           >
@@ -91,7 +92,7 @@ class RenderSearchResults extends React.Component<PropsWithContext> {
           </Animated.View>
         }
       />
-    </View>
+    </React.Fragment>
   );
 }
 type Props = {|
@@ -107,12 +108,6 @@ const RenderSearchResultsWithContext = (props: Props) => (
   </SearchResultsContext.Consumer>
 );
 
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-  },
-});
-
 export default createFragmentContainer(
   RenderSearchResultsWithContext,
   graphql`
diff --git a/app/hotels/src/allHotels/__generated__/NewAllHotelsSearchQuery.graphql.js b/app/hotels/src/allHotels/__generated__/NewAllHotelsSearchQuery.graphql.js
index 0da11646..da7e163e 100644
--- a/app/hotels/src/allHotels/__generated__/NewAllHotelsSearchQuery.graphql.js
+++ b/app/hotels/src/allHotels/__generated__/NewAllHotelsSearchQuery.graphql.js
@@ -1,6 +1,6 @@
 /**
  * @flow
- * @relayHash 042fd999d0aa8f1eb05849ec37b0df6e
+ * @relayHash 284728f793100f1357f43f9ebf21264c
  */
 
 /* eslint-disable */
@@ -181,20 +181,25 @@ fragment HotelDetailPreview_availability on HotelAvailability {
     currency
   }
   hotel {
+    ...HotelReviewScore_hotel
     id
     name
     mainPhoto {
       thumbnailUrl
       id
     }
-    review {
-      score
-      description
-      count
+    rating {
+      stars
     }
   }
 }
 
+fragment HotelReviewScore_hotel on Hotel {
+  review {
+    score
+  }
+}
+
 fragment PriceMarker on Price {
   amount
   currency
@@ -227,12 +232,6 @@ fragment HotelTitle on HotelAvailability {
   }
 }
 
-fragment HotelReviewScore_hotel on Hotel {
-  review {
-    score
-  }
-}
-
 fragment HotelDistance_hotel on Hotel {
   distanceFromCenter
 }
@@ -315,7 +314,7 @@ return {
   "operationKind": "query",
   "name": "NewAllHotelsSearchQuery",
   "id": null,
-  "text": "query NewAllHotelsSearchQuery(\n  $search: HotelsSearchInput!\n  $filter: HotelsFilterInput!\n  $options: AvailableHotelOptionsInput\n  $first: Int\n  $after: String\n) {\n  ...RenderSearchResults\n}\n\nfragment RenderSearchResults on RootQuery {\n  ...AllHotelsSearchList_data\n  ...MapScreen\n}\n\nfragment AllHotelsSearchList_data on RootQuery {\n  allAvailableHotels(search: $search, filter: $filter, options: $options, first: $first, after: $after) {\n    pageInfo {\n      hasNextPage\n      hasPreviousPage\n      startCursor\n      endCursor\n    }\n    edges {\n      node {\n        id\n        ...AllHotelsSearchRow\n        __typename\n      }\n      ...MapView\n      cursor\n    }\n    stats {\n      maxPrice\n      minPrice\n    }\n  }\n}\n\nfragment MapScreen on RootQuery {\n  allAvailableHotels(search: $search, filter: $filter, options: $options, first: $first, after: $after) {\n    pageInfo {\n      hasNextPage\n      hasPreviousPage\n      startCursor\n      endCursor\n    }\n    edges {\n      node {\n        id\n        __typename\n      }\n      ...MapView\n      ...HotelSwipeList\n      cursor\n    }\n    stats {\n      maxPrice\n      minPrice\n    }\n  }\n}\n\nfragment MapView on HotelAvailabilityEdge {\n  node {\n    id\n    price {\n      ...PriceMarker\n    }\n    hotel {\n      coordinates {\n        lat\n        lng\n      }\n      id\n    }\n  }\n}\n\nfragment HotelSwipeList on HotelAvailabilityEdge {\n  node {\n    id\n    ...HotelSwipeItem\n    hotel {\n      address {\n        ...Address_address\n      }\n      id\n    }\n  }\n}\n\nfragment HotelSwipeItem on HotelAvailability {\n  ...HotelDetailPreview_availability\n  hotel {\n    id\n  }\n}\n\nfragment Address_address on Address {\n  street\n  city\n  zip\n}\n\nfragment HotelDetailPreview_availability on HotelAvailability {\n  price {\n    amount\n    currency\n  }\n  hotel {\n    id\n    name\n    mainPhoto {\n      thumbnailUrl\n      id\n    }\n    review {\n      score\n      description\n      count\n    }\n  }\n}\n\nfragment PriceMarker on Price {\n  amount\n  currency\n}\n\nfragment AllHotelsSearchRow on HotelAvailability {\n  ...HotelTitle\n  hotel {\n    id\n    mainPhoto {\n      lowResUrl\n      id\n    }\n    ...HotelReviewScore_hotel\n  }\n}\n\nfragment HotelTitle on HotelAvailability {\n  price {\n    amount\n    currency\n  }\n  hotel {\n    ...HotelDistance_hotel\n    name\n    rating {\n      stars\n    }\n    id\n  }\n}\n\nfragment HotelReviewScore_hotel on Hotel {\n  review {\n    score\n  }\n}\n\nfragment HotelDistance_hotel on Hotel {\n  distanceFromCenter\n}\n",
+  "text": "query NewAllHotelsSearchQuery(\n  $search: HotelsSearchInput!\n  $filter: HotelsFilterInput!\n  $options: AvailableHotelOptionsInput\n  $first: Int\n  $after: String\n) {\n  ...RenderSearchResults\n}\n\nfragment RenderSearchResults on RootQuery {\n  ...AllHotelsSearchList_data\n  ...MapScreen\n}\n\nfragment AllHotelsSearchList_data on RootQuery {\n  allAvailableHotels(search: $search, filter: $filter, options: $options, first: $first, after: $after) {\n    pageInfo {\n      hasNextPage\n      hasPreviousPage\n      startCursor\n      endCursor\n    }\n    edges {\n      node {\n        id\n        ...AllHotelsSearchRow\n        __typename\n      }\n      ...MapView\n      cursor\n    }\n    stats {\n      maxPrice\n      minPrice\n    }\n  }\n}\n\nfragment MapScreen on RootQuery {\n  allAvailableHotels(search: $search, filter: $filter, options: $options, first: $first, after: $after) {\n    pageInfo {\n      hasNextPage\n      hasPreviousPage\n      startCursor\n      endCursor\n    }\n    edges {\n      node {\n        id\n        __typename\n      }\n      ...MapView\n      ...HotelSwipeList\n      cursor\n    }\n    stats {\n      maxPrice\n      minPrice\n    }\n  }\n}\n\nfragment MapView on HotelAvailabilityEdge {\n  node {\n    id\n    price {\n      ...PriceMarker\n    }\n    hotel {\n      coordinates {\n        lat\n        lng\n      }\n      id\n    }\n  }\n}\n\nfragment HotelSwipeList on HotelAvailabilityEdge {\n  node {\n    id\n    ...HotelSwipeItem\n    hotel {\n      address {\n        ...Address_address\n      }\n      id\n    }\n  }\n}\n\nfragment HotelSwipeItem on HotelAvailability {\n  ...HotelDetailPreview_availability\n  hotel {\n    id\n  }\n}\n\nfragment Address_address on Address {\n  street\n  city\n  zip\n}\n\nfragment HotelDetailPreview_availability on HotelAvailability {\n  price {\n    amount\n    currency\n  }\n  hotel {\n    ...HotelReviewScore_hotel\n    id\n    name\n    mainPhoto {\n      thumbnailUrl\n      id\n    }\n    rating {\n      stars\n    }\n  }\n}\n\nfragment HotelReviewScore_hotel on Hotel {\n  review {\n    score\n  }\n}\n\nfragment PriceMarker on Price {\n  amount\n  currency\n}\n\nfragment AllHotelsSearchRow on HotelAvailability {\n  ...HotelTitle\n  hotel {\n    id\n    mainPhoto {\n      lowResUrl\n      id\n    }\n    ...HotelReviewScore_hotel\n  }\n}\n\nfragment HotelTitle on HotelAvailability {\n  price {\n    amount\n    currency\n  }\n  hotel {\n    ...HotelDistance_hotel\n    name\n    rating {\n      stars\n    }\n    id\n  }\n}\n\nfragment HotelDistance_hotel on Hotel {\n  distanceFromCenter\n}\n",
   "metadata": {},
   "fragment": {
     "kind": "Fragment",
@@ -511,20 +510,6 @@ return {
                             "name": "score",
                             "args": null,
                             "storageKey": null
-                          },
-                          {
-                            "kind": "ScalarField",
-                            "alias": null,
-                            "name": "description",
-                            "args": null,
-                            "storageKey": null
-                          },
-                          {
-                            "kind": "ScalarField",
-                            "alias": null,
-                            "name": "count",
-                            "args": null,
-                            "storageKey": null
                           }
                         ]
                       },
diff --git a/app/hotels/src/allHotels/HotelReviewScore.js b/app/hotels/src/components/HotelReviewScore.js
similarity index 100%
rename from app/hotels/src/allHotels/HotelReviewScore.js
rename to app/hotels/src/components/HotelReviewScore.js
index f7b2a0f4..5ac1b198 100644
--- a/app/hotels/src/allHotels/HotelReviewScore.js
+++ b/app/hotels/src/components/HotelReviewScore.js
@@ -13,25 +13,6 @@ type Props = {|
   +hotel: HotelReviewScore_hotel,
 |};
 
-const style = StyleSheet.create({
-  adaptableBadge: {
-    android: {
-      paddingHorizontal: 8,
-    },
-    ios: {
-      paddingHorizontal: 10,
-    },
-  },
-  adaptableBadgeText: {
-    android: {
-      fontWeight: 'bold',
-    },
-    ios: {
-      fontWeight: '600',
-    },
-  },
-});
-
 export class HotelReviewScore extends React.Component<Props> {
   /**
    * One decimal point with trailing zero.
@@ -86,6 +67,25 @@ export class HotelReviewScore extends React.Component<Props> {
   };
 }
 
+const style = StyleSheet.create({
+  adaptableBadge: {
+    android: {
+      paddingHorizontal: 8,
+    },
+    ios: {
+      paddingHorizontal: 10,
+    },
+  },
+  adaptableBadgeText: {
+    android: {
+      fontWeight: 'bold',
+    },
+    ios: {
+      fontWeight: '600',
+    },
+  },
+});
+
 export default createFragmentContainer(
   HotelReviewScore,
   graphql`
diff --git a/app/hotels/src/allHotels/__generated__/HotelReviewScore_hotel.graphql.js b/app/hotels/src/components/__generated__/HotelReviewScore_hotel.graphql.js
similarity index 100%
rename from app/hotels/src/allHotels/__generated__/HotelReviewScore_hotel.graphql.js
rename to app/hotels/src/components/__generated__/HotelReviewScore_hotel.graphql.js
diff --git a/app/hotels/src/allHotels/__tests__/HotelReviewScore.test.js b/app/hotels/src/components/__tests__/HotelReviewScore.test.js
similarity index 100%
rename from app/hotels/src/allHotels/__tests__/HotelReviewScore.test.js
rename to app/hotels/src/components/__tests__/HotelReviewScore.test.js
diff --git a/app/hotels/src/map/HotelDetailPreview.js b/app/hotels/src/map/HotelDetailPreview.js
deleted file mode 100644
index 4cd2a86e..00000000
--- a/app/hotels/src/map/HotelDetailPreview.js
+++ /dev/null
@@ -1,148 +0,0 @@
-// @flow strict
-
-import * as React from 'react';
-import { View } from 'react-native';
-import { StyleSheet, NetworkImage, Price, Text } from '@kiwicom/mobile-shared';
-import { createFragmentContainer, graphql } from '@kiwicom/mobile-relay';
-import { Translation } from '@kiwicom/mobile-localization';
-import idx from 'idx';
-import { defaultTokens } from '@kiwicom/mobile-orbit';
-
-import type { HotelDetailPreview_availability } from './__generated__/HotelDetailPreview_availability.graphql';
-
-type ContainerProps = {|
-  +availability: $FlowFixMeProps,
-|};
-
-type Props = {|
-  ...ContainerProps,
-  +availability: ?HotelDetailPreview_availability,
-|};
-
-const getReview = review => {
-  if (review && review.count !== 0) {
-    const reviewScore = review.score || 0;
-    const reviewDescription = review.description || '';
-    const reviewCount = review.count || 0;
-
-    const delimiter =
-      Number.isFinite(review.score) || review.description != null ? ' · ' : '';
-
-    return (
-      <Text style={styles.metainfo}>
-        <Translation passThrough={`${reviewScore} ${reviewDescription}`} />
-        <Translation passThrough={delimiter} />
-        <Translation
-          id="hotels.map.multiple_reviews"
-          values={{ numberOfReviews: reviewCount }}
-        />
-      </Text>
-    );
-  }
-
-  return (
-    <Text style={styles.metainfo}>
-      <Translation id="hotels.map.no_reviews" />
-    </Text>
-  );
-};
-
-export class HotelDetailPreview extends React.Component<Props> {
-  render = () => {
-    const { availability } = this.props;
-    const name = idx(availability, _ => _.hotel.name);
-    const price = idx(availability, _ => _.price) || {};
-    const image = idx(availability, _ => _.hotel.mainPhoto.thumbnailUrl);
-    const review = idx(availability, _ => _.hotel.review);
-
-    return (
-      <View style={styles.container}>
-        <View>
-          <NetworkImage
-            style={styles.image}
-            source={{
-              uri: image,
-            }}
-            resizeMode="contain"
-          />
-        </View>
-        <View style={styles.description}>
-          <Text style={styles.hotelName} numberOfLines={1}>
-            <Translation passThrough={name} />
-          </Text>
-          {getReview(review)}
-          {price &&
-            price.currency != null &&
-            price.amount != null && (
-              <Text style={styles.price}>
-                <Price currency={price.currency} amount={price.amount} />
-              </Text>
-            )}
-        </View>
-      </View>
-    );
-  };
-}
-
-const styles = StyleSheet.create({
-  container: {
-    flexDirection: 'row',
-    height: 60,
-  },
-  image: {
-    width: 60,
-    height: 60,
-    borderRadius: 3,
-    marginEnd: 10,
-  },
-  description: {
-    flex: 1,
-    justifyContent: 'space-between',
-  },
-  hotelName: {
-    fontSize: 16,
-    color: defaultTokens.colorTextAttention,
-    android: {
-      lineHeight: 17,
-    },
-  },
-  metainfo: {
-    color: defaultTokens.colorTextSecondary,
-    fontSize: 12,
-    android: {
-      lineHeight: 13,
-    },
-  },
-  price: {
-    color: defaultTokens.paletteProductNormal,
-    fontWeight: 'bold',
-    fontSize: 12,
-    android: {
-      lineHeight: 13,
-    },
-  },
-});
-
-export default (createFragmentContainer(
-  HotelDetailPreview,
-  graphql`
-    fragment HotelDetailPreview_availability on HotelAvailability {
-      price {
-        amount
-        currency
-      }
-      hotel {
-        id
-        name
-        mainPhoto {
-          thumbnailUrl
-        }
-        review {
-          score
-          description
-          count
-        }
-      }
-    }
-  `,
-): React.ComponentType<ContainerProps>);
diff --git a/app/hotels/src/map/allHotels/HotelSwipeItem.js b/app/hotels/src/map/allHotels/HotelSwipeItem.js
index 7ddd8057..031ab2f2 100644
--- a/app/hotels/src/map/allHotels/HotelSwipeItem.js
+++ b/app/hotels/src/map/allHotels/HotelSwipeItem.js
@@ -6,7 +6,7 @@ import { createFragmentContainer, graphql } from '@kiwicom/mobile-relay';
 import idx from 'idx';
 import { TouchableWithoutFeedback } from '@kiwicom/mobile-shared';
 
-import HotelDetailPreview from '../HotelDetailPreview';
+import HotelDetailPreview from '../hotelDetailPreview/HotelDetailPreview';
 import type { HotelSwipeItem as HotelSwipeItemData } from './__generated__/HotelSwipeItem.graphql';
 
 type ContainerProps = {|
diff --git a/app/hotels/src/map/allHotels/HotelSwipeList.js b/app/hotels/src/map/allHotels/HotelSwipeList.js
index 192df872..44e02a93 100644
--- a/app/hotels/src/map/allHotels/HotelSwipeList.js
+++ b/app/hotels/src/map/allHotels/HotelSwipeList.js
@@ -8,7 +8,7 @@ import {
   BottomSheet,
   Device,
   StyleSheet,
-  AdaptableLayout,
+  type OnLayout,
 } from '@kiwicom/mobile-shared';
 import { Translation } from '@kiwicom/mobile-localization';
 import idx from 'idx';
@@ -18,45 +18,28 @@ import Address from '../Address';
 import type { HotelSwipeList as HotelSwipeListData } from './__generated__/HotelSwipeList.graphql';
 import { openHeight, closedHeight } from '../bottomSheetDimensions';
 import BottomSheetHandle from '../BottomSheetHandle';
+import { HotelPreviewProvider } from '../hotelDetailPreview/HotelDetailPreviewContext';
 
 type Props = {|
-  data: HotelSwipeListData,
-  selectedIndex: number,
-  onSnapToItem: (index: number) => void,
-  onOpenSingleHotel: (hotelId: string) => void,
+  +data: HotelSwipeListData,
+  +selectedIndex: number,
+  +onSnapToItem: (index: number) => void,
+  +onOpenSingleHotel: (hotelId: string) => void,
 |};
 
 type State = {|
-  screenWidth: number,
+  containerWidth: number,
 |};
 
 const SNAP_WIDTH = 0.8;
-
-const styles = StyleSheet.create({
-  sliderWrapper: {
-    height: closedHeight,
-  },
-  slider: {
-    paddingTop: 5,
-    paddingHorizontal: 10,
-    paddingBottom: 10,
-  },
-  fullWidth: {
-    width: '100%',
-  },
-  wide: {
-    maxWidth: Device.DEVICE_THRESHOLD,
-  },
-  noResultsContainer: {
-    alignItems: 'center',
-    marginTop: 10,
-  },
-});
-
+const PADDING_HORIZONTAL = 8;
 const CARD_ITEM_WIDTH = Device.DEVICE_THRESHOLD * SNAP_WIDTH;
 
 class HotelSwipeList extends React.Component<Props, State> {
   carouselRef: React.ElementRef<typeof Carousel>;
+  state = {
+    containerWidth: 0,
+  };
 
   componentDidUpdate = () => {
     if (this.carouselRef) {
@@ -86,52 +69,76 @@ class HotelSwipeList extends React.Component<Props, State> {
     this.carouselRef = ref;
   };
 
+  onLayout = (e: OnLayout) => {
+    const containerPadding = PADDING_HORIZONTAL * 2;
+    this.setState({
+      containerWidth: e.nativeEvent.layout.width - containerPadding,
+    });
+  };
+
   render = () => {
     const { data, onSnapToItem } = this.props;
 
-    const child = (
-      <BottomSheet openHeight={openHeight} closedHeight={closedHeight}>
-        <BottomSheetHandle />
-        {data.length ? (
-          <React.Fragment>
-            <View style={styles.sliderWrapper}>
-              <Carousel
-                ref={this.storeRef}
-                data={data}
-                renderItem={this.renderItem}
-                sliderWidth={Device.DEVICE_THRESHOLD}
-                itemWidth={CARD_ITEM_WIDTH}
-                inactiveSlideScale={1}
-                inactiveSlideOpacity={0.5}
-                decelerationRate="fast"
-                activeSlideAlignment="start"
-                containerCustomStyle={styles.slider}
-                removeClippedSubviews={false}
-                onSnapToItem={onSnapToItem}
-                useScrollView={true}
-              />
-            </View>
-            <Address address={this.getSelectedAddress()} />
-          </React.Fragment>
-        ) : (
-          <View style={styles.noResultsContainer}>
-            <Translation id="hotels.map.no_results" />
-          </View>
-        )}
-      </BottomSheet>
-    );
-
     return (
-      <AdaptableLayout
-        renderOnWide={
-          <View style={[styles.fullWidth, styles.wide]}>{child}</View>
-        }
-        renderOnNarrow={<View style={styles.fullWidth}>{child}</View>}
-      />
+      <View style={styles.container} onLayout={this.onLayout}>
+        <HotelPreviewProvider value={this.state}>
+          <BottomSheet openHeight={openHeight} closedHeight={closedHeight}>
+            <BottomSheetHandle />
+            {data.length ? (
+              <React.Fragment>
+                <View style={styles.sliderWrapper}>
+                  <Carousel
+                    ref={this.storeRef}
+                    data={data}
+                    renderItem={this.renderItem}
+                    sliderWidth={Device.DEVICE_THRESHOLD}
+                    itemWidth={CARD_ITEM_WIDTH}
+                    inactiveSlideScale={1}
+                    inactiveSlideOpacity={0.5}
+                    decelerationRate="fast"
+                    activeSlideAlignment="start"
+                    containerCustomStyle={styles.slider}
+                    removeClippedSubviews={false}
+                    onSnapToItem={onSnapToItem}
+                    useScrollView={true}
+                  />
+                </View>
+                <Address address={this.getSelectedAddress()} />
+              </React.Fragment>
+            ) : (
+              <View style={styles.noResultsContainer}>
+                <Translation id="hotels.map.no_results" />
+              </View>
+            )}
+          </BottomSheet>
+        </HotelPreviewProvider>
+      </View>
     );
   };
 }
 
+const styles = StyleSheet.create({
+  sliderWrapper: {
+    height: closedHeight,
+    borderRadius: 6,
+  },
+  slider: {
+    paddingTop: 5,
+    paddingHorizontal: PADDING_HORIZONTAL,
+    paddingBottom: 8,
+  },
+  container: {
+    position: 'absolute',
+    bottom: Device.isIPhoneX ? 88 : 60,
+    start: 8,
+    end: 8,
+  },
+  noResultsContainer: {
+    alignItems: 'center',
+    marginTop: 10,
+  },
+});
+
 export default createFragmentContainer(
   HotelSwipeList,
   graphql`
diff --git a/app/hotels/src/map/allHotels/MapScreen.js b/app/hotels/src/map/allHotels/MapScreen.js
index 53b7df53..caefeef1 100644
--- a/app/hotels/src/map/allHotels/MapScreen.js
+++ b/app/hotels/src/map/allHotels/MapScreen.js
@@ -19,14 +19,6 @@ type State = {|
   selectedHotelIndex: number,
 |};
 
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-    justifyContent: 'flex-end',
-    alignItems: 'center',
-  },
-});
-
 class MapScreen extends React.Component<Props, State> {
   state = {
     selectedHotelIndex: 0,
@@ -81,6 +73,15 @@ class MapScreen extends React.Component<Props, State> {
   };
 }
 
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    justifyContent: 'flex-end',
+    alignItems: 'center',
+    padding: 8,
+  },
+});
+
 export default createFragmentContainer(
   MapScreen,
   graphql`
diff --git a/app/hotels/src/map/allHotels/__generated__/NewAllHotelsMapQuery.graphql.js b/app/hotels/src/map/allHotels/__generated__/NewAllHotelsMapQuery.graphql.js
index d25c630f..ee1dcaf0 100644
--- a/app/hotels/src/map/allHotels/__generated__/NewAllHotelsMapQuery.graphql.js
+++ b/app/hotels/src/map/allHotels/__generated__/NewAllHotelsMapQuery.graphql.js
@@ -1,6 +1,6 @@
 /**
  * @flow
- * @relayHash ed8125b0916897691f44a784026507c8
+ * @relayHash 71db383f66d5d77ab0a9bb91c82525ed
  */
 
 /* eslint-disable */
@@ -152,20 +152,25 @@ fragment HotelDetailPreview_availability on HotelAvailability {
     currency
   }
   hotel {
+    ...HotelReviewScore_hotel
     id
     name
     mainPhoto {
       thumbnailUrl
       id
     }
-    review {
-      score
-      description
-      count
+    rating {
+      stars
     }
   }
 }
 
+fragment HotelReviewScore_hotel on Hotel {
+  review {
+    score
+  }
+}
+
 fragment PriceMarker on Price {
   amount
   currency
@@ -249,7 +254,7 @@ return {
   "operationKind": "query",
   "name": "NewAllHotelsMapQuery",
   "id": null,
-  "text": "query NewAllHotelsMapQuery(\n  $search: HotelsSearchInput!\n  $filter: HotelsFilterInput\n  $options: AvailableHotelOptionsInput\n  $first: Int\n  $after: String\n) {\n  ...MapScreen\n}\n\nfragment MapScreen on RootQuery {\n  allAvailableHotels(search: $search, filter: $filter, options: $options, first: $first, after: $after) {\n    pageInfo {\n      hasNextPage\n      hasPreviousPage\n      startCursor\n      endCursor\n    }\n    edges {\n      node {\n        id\n        __typename\n      }\n      ...MapView\n      ...HotelSwipeList\n      cursor\n    }\n    stats {\n      maxPrice\n      minPrice\n    }\n  }\n}\n\nfragment MapView on HotelAvailabilityEdge {\n  node {\n    id\n    price {\n      ...PriceMarker\n    }\n    hotel {\n      coordinates {\n        lat\n        lng\n      }\n      id\n    }\n  }\n}\n\nfragment HotelSwipeList on HotelAvailabilityEdge {\n  node {\n    id\n    ...HotelSwipeItem\n    hotel {\n      address {\n        ...Address_address\n      }\n      id\n    }\n  }\n}\n\nfragment HotelSwipeItem on HotelAvailability {\n  ...HotelDetailPreview_availability\n  hotel {\n    id\n  }\n}\n\nfragment Address_address on Address {\n  street\n  city\n  zip\n}\n\nfragment HotelDetailPreview_availability on HotelAvailability {\n  price {\n    amount\n    currency\n  }\n  hotel {\n    id\n    name\n    mainPhoto {\n      thumbnailUrl\n      id\n    }\n    review {\n      score\n      description\n      count\n    }\n  }\n}\n\nfragment PriceMarker on Price {\n  amount\n  currency\n}\n",
+  "text": "query NewAllHotelsMapQuery(\n  $search: HotelsSearchInput!\n  $filter: HotelsFilterInput\n  $options: AvailableHotelOptionsInput\n  $first: Int\n  $after: String\n) {\n  ...MapScreen\n}\n\nfragment MapScreen on RootQuery {\n  allAvailableHotels(search: $search, filter: $filter, options: $options, first: $first, after: $after) {\n    pageInfo {\n      hasNextPage\n      hasPreviousPage\n      startCursor\n      endCursor\n    }\n    edges {\n      node {\n        id\n        __typename\n      }\n      ...MapView\n      ...HotelSwipeList\n      cursor\n    }\n    stats {\n      maxPrice\n      minPrice\n    }\n  }\n}\n\nfragment MapView on HotelAvailabilityEdge {\n  node {\n    id\n    price {\n      ...PriceMarker\n    }\n    hotel {\n      coordinates {\n        lat\n        lng\n      }\n      id\n    }\n  }\n}\n\nfragment HotelSwipeList on HotelAvailabilityEdge {\n  node {\n    id\n    ...HotelSwipeItem\n    hotel {\n      address {\n        ...Address_address\n      }\n      id\n    }\n  }\n}\n\nfragment HotelSwipeItem on HotelAvailability {\n  ...HotelDetailPreview_availability\n  hotel {\n    id\n  }\n}\n\nfragment Address_address on Address {\n  street\n  city\n  zip\n}\n\nfragment HotelDetailPreview_availability on HotelAvailability {\n  price {\n    amount\n    currency\n  }\n  hotel {\n    ...HotelReviewScore_hotel\n    id\n    name\n    mainPhoto {\n      thumbnailUrl\n      id\n    }\n    rating {\n      stars\n    }\n  }\n}\n\nfragment HotelReviewScore_hotel on Hotel {\n  review {\n    score\n  }\n}\n\nfragment PriceMarker on Price {\n  amount\n  currency\n}\n",
   "metadata": {},
   "fragment": {
     "kind": "Fragment",
@@ -397,6 +402,24 @@ return {
                         ]
                       },
                       v2,
+                      {
+                        "kind": "LinkedField",
+                        "alias": null,
+                        "name": "review",
+                        "storageKey": null,
+                        "args": null,
+                        "concreteType": "HotelReview",
+                        "plural": false,
+                        "selections": [
+                          {
+                            "kind": "ScalarField",
+                            "alias": null,
+                            "name": "score",
+                            "args": null,
+                            "storageKey": null
+                          }
+                        ]
+                      },
                       {
                         "kind": "ScalarField",
                         "alias": null,
@@ -426,30 +449,16 @@ return {
                       {
                         "kind": "LinkedField",
                         "alias": null,
-                        "name": "review",
+                        "name": "rating",
                         "storageKey": null,
                         "args": null,
-                        "concreteType": "HotelReview",
+                        "concreteType": "HotelRating",
                         "plural": false,
                         "selections": [
                           {
                             "kind": "ScalarField",
                             "alias": null,
-                            "name": "score",
-                            "args": null,
-                            "storageKey": null
-                          },
-                          {
-                            "kind": "ScalarField",
-                            "alias": null,
-                            "name": "description",
-                            "args": null,
-                            "storageKey": null
-                          },
-                          {
-                            "kind": "ScalarField",
-                            "alias": null,
-                            "name": "count",
+                            "name": "stars",
                             "args": null,
                             "storageKey": null
                           }
diff --git a/app/hotels/src/map/hotelDetailPreview/HotelDetailPreview.js b/app/hotels/src/map/hotelDetailPreview/HotelDetailPreview.js
new file mode 100644
index 00000000..c14e5cfe
--- /dev/null
+++ b/app/hotels/src/map/hotelDetailPreview/HotelDetailPreview.js
@@ -0,0 +1,125 @@
+// @flow strict
+
+import * as React from 'react';
+import { View } from 'react-native';
+import {
+  StyleSheet,
+  NetworkImage,
+  Price,
+  Text,
+  Stars,
+} from '@kiwicom/mobile-shared';
+import { createFragmentContainer, graphql } from '@kiwicom/mobile-relay';
+import { Translation } from '@kiwicom/mobile-localization';
+import idx from 'idx';
+import { defaultTokens } from '@kiwicom/mobile-orbit';
+
+import type { HotelDetailPreview_availability } from './__generated__/HotelDetailPreview_availability.graphql';
+import { HotelDetailConsumer } from './HotelDetailPreviewContext';
+import HotelReviewScore from '../../components/HotelReviewScore';
+
+type Props = {|
+  +availability: ?HotelDetailPreview_availability,
+|};
+
+export const HotelDetailPreview = ({ availability }: Props) => {
+  const name = idx(availability, _ => _.hotel.name);
+  const price = idx(availability, _ => _.price) || {};
+  const image = idx(availability, _ => _.hotel.mainPhoto.thumbnailUrl);
+
+  return (
+    <HotelDetailConsumer>
+      {({ containerWidth }) => (
+        <View style={[styles.container, { width: containerWidth }]}>
+          <View>
+            <NetworkImage
+              style={styles.image}
+              source={{
+                uri: image,
+              }}
+              resizeMode="contain"
+            />
+          </View>
+          <View style={styles.description}>
+            <Text style={styles.hotelName} numberOfLines={1}>
+              <Translation passThrough={name} />
+            </Text>
+            <View style={styles.row}>
+              <Stars
+                rating={idx(availability, _ => _.hotel.rating.stars)}
+                style={styles.stars}
+              />
+              <HotelReviewScore hotel={idx(availability, _ => _.hotel)} />
+            </View>
+            {price &&
+              price.currency != null &&
+              price.amount != null && (
+                <Text style={styles.price}>
+                  <Price currency={price.currency} amount={price.amount} />
+                </Text>
+              )}
+          </View>
+        </View>
+      )}
+    </HotelDetailConsumer>
+  );
+};
+
+const styles = StyleSheet.create({
+  container: {
+    flexDirection: 'row',
+    height: 60,
+  },
+  image: {
+    width: 60,
+    height: 60,
+    borderRadius: 3,
+    marginEnd: 10,
+  },
+  description: {
+    flex: 1,
+    justifyContent: 'space-between',
+  },
+  hotelName: {
+    fontWeight: '500',
+  },
+  price: {
+    color: defaultTokens.paletteProductNormal,
+    fontWeight: 'bold',
+    fontSize: 12,
+    android: {
+      lineHeight: 13,
+    },
+  },
+  stars: {
+    color: defaultTokens.paletteInkLight,
+  },
+  row: {
+    flex: 1,
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+  },
+});
+
+export default createFragmentContainer(
+  HotelDetailPreview,
+  graphql`
+    fragment HotelDetailPreview_availability on HotelAvailability {
+      price {
+        amount
+        currency
+      }
+      hotel {
+        ...HotelReviewScore_hotel
+        id
+        name
+        mainPhoto {
+          thumbnailUrl
+        }
+        rating {
+          stars
+        }
+      }
+    }
+  `,
+);
diff --git a/app/hotels/src/map/hotelDetailPreview/HotelDetailPreviewContext.js b/app/hotels/src/map/hotelDetailPreview/HotelDetailPreviewContext.js
new file mode 100644
index 00000000..5b63ef84
--- /dev/null
+++ b/app/hotels/src/map/hotelDetailPreview/HotelDetailPreviewContext.js
@@ -0,0 +1,11 @@
+// @flow strict
+
+import * as React from 'react';
+
+const defaultContextValue = {
+  containerWidth: 0,
+};
+
+const { Provider, Consumer } = React.createContext(defaultContextValue);
+
+export { Provider as HotelPreviewProvider, Consumer as HotelDetailConsumer };
diff --git a/app/hotels/src/map/__generated__/HotelDetailPreview_availability.graphql.js b/app/hotels/src/map/hotelDetailPreview/__generated__/HotelDetailPreview_availability.graphql.js
similarity index 80%
rename from app/hotels/src/map/__generated__/HotelDetailPreview_availability.graphql.js
rename to app/hotels/src/map/hotelDetailPreview/__generated__/HotelDetailPreview_availability.graphql.js
index 153cc9a3..06317e4c 100644
--- a/app/hotels/src/map/__generated__/HotelDetailPreview_availability.graphql.js
+++ b/app/hotels/src/map/hotelDetailPreview/__generated__/HotelDetailPreview_availability.graphql.js
@@ -8,6 +8,7 @@
 
 /*::
 import type { ConcreteFragment } from 'relay-runtime';
+type HotelReviewScore_hotel$ref = any;
 import type { FragmentReference } from "relay-runtime";
 declare export opaque type HotelDetailPreview_availability$ref: FragmentReference;
 export type HotelDetailPreview_availability = {|
@@ -21,11 +22,10 @@ export type HotelDetailPreview_availability = {|
     +mainPhoto: ?{|
       +thumbnailUrl: ?string
     |},
-    +review: ?{|
-      +score: ?number,
-      +description: ?string,
-      +count: ?number,
+    +rating: ?{|
+      +stars: ?number
     |},
+    +$fragmentRefs: HotelReviewScore_hotel$ref,
   |},
   +$refType: HotelDetailPreview_availability$ref,
 |};
@@ -73,6 +73,11 @@ const node/*: ConcreteFragment*/ = {
       "concreteType": "Hotel",
       "plural": false,
       "selections": [
+        {
+          "kind": "FragmentSpread",
+          "name": "HotelReviewScore_hotel",
+          "args": null
+        },
         {
           "kind": "ScalarField",
           "alias": null,
@@ -108,30 +113,16 @@ const node/*: ConcreteFragment*/ = {
         {
           "kind": "LinkedField",
           "alias": null,
-          "name": "review",
+          "name": "rating",
           "storageKey": null,
           "args": null,
-          "concreteType": "HotelReview",
+          "concreteType": "HotelRating",
           "plural": false,
           "selections": [
             {
               "kind": "ScalarField",
               "alias": null,
-              "name": "score",
-              "args": null,
-              "storageKey": null
-            },
-            {
-              "kind": "ScalarField",
-              "alias": null,
-              "name": "description",
-              "args": null,
-              "storageKey": null
-            },
-            {
-              "kind": "ScalarField",
-              "alias": null,
-              "name": "count",
+              "name": "stars",
               "args": null,
               "storageKey": null
             }
@@ -142,5 +133,5 @@ const node/*: ConcreteFragment*/ = {
   ]
 };
 // prettier-ignore
-(node/*: any*/).hash = '7f97f501a2c5c8f1394094641853ce5c';
+(node/*: any*/).hash = 'f7e9077bb6cc45f9e9c9cd130eb48d2f';
 module.exports = node;
diff --git a/app/hotels/src/map/singleHotel/AdditionalInfo.js b/app/hotels/src/map/singleHotel/AdditionalInfo.js
index 3a78df4e..25bb9c9c 100644
--- a/app/hotels/src/map/singleHotel/AdditionalInfo.js
+++ b/app/hotels/src/map/singleHotel/AdditionalInfo.js
@@ -7,7 +7,7 @@ import { createFragmentContainer, graphql } from '@kiwicom/mobile-relay';
 import { StyleSheet } from '@kiwicom/mobile-shared';
 
 import BottomSheet from './BottomSheet';
-import HotelDetailPreview from '../HotelDetailPreview';
+import HotelDetailPreview from '../hotelDetailPreview/HotelDetailPreview';
 import Address from '../Address';
 import type { AdditionalInfo as AdditionalInfoData } from './__generated__/AdditionalInfo.graphql';
 import BottomSheetHandle from '../BottomSheetHandle';
diff --git a/app/hotels/src/map/singleHotel/__generated__/SingleHotelMapScreenQuery.graphql.js b/app/hotels/src/map/singleHotel/__generated__/SingleHotelMapScreenQuery.graphql.js
index b286b87e..9031f71a 100644
--- a/app/hotels/src/map/singleHotel/__generated__/SingleHotelMapScreenQuery.graphql.js
+++ b/app/hotels/src/map/singleHotel/__generated__/SingleHotelMapScreenQuery.graphql.js
@@ -1,6 +1,6 @@
 /**
  * @flow
- * @relayHash 1328769eb5346ce5ef5fbdae61821a6e
+ * @relayHash e64c2ba9d2e703fe591d49f8a452e62f
  */
 
 /* eslint-disable */
@@ -87,16 +87,15 @@ fragment HotelDetailPreview_availability on HotelAvailability {
     currency
   }
   hotel {
+    ...HotelReviewScore_hotel
     id
     name
     mainPhoto {
       thumbnailUrl
       id
     }
-    review {
-      score
-      description
-      count
+    rating {
+      stars
     }
   }
 }
@@ -106,6 +105,12 @@ fragment Address_address on Address {
   city
   zip
 }
+
+fragment HotelReviewScore_hotel on Hotel {
+  review {
+    score
+  }
+}
 */
 
 const node/*: ConcreteRequest*/ = (function(){
@@ -149,7 +154,7 @@ return {
   "operationKind": "query",
   "name": "SingleHotelMapScreenQuery",
   "id": null,
-  "text": "query SingleHotelMapScreenQuery(\n  $search: AvailableHotelSearchInput!\n  $options: AvailableHotelOptionsInput\n) {\n  availableHotel(search: $search, options: $options) {\n    hotel {\n      ...MapView_hotel\n      id\n    }\n    ...AdditionalInfo\n    id\n  }\n}\n\nfragment MapView_hotel on Hotel {\n  coordinates {\n    lat\n    lng\n  }\n}\n\nfragment AdditionalInfo on HotelAvailability {\n  ...HotelDetailPreview_availability\n  hotel {\n    address {\n      ...Address_address\n    }\n    id\n  }\n}\n\nfragment HotelDetailPreview_availability on HotelAvailability {\n  price {\n    amount\n    currency\n  }\n  hotel {\n    id\n    name\n    mainPhoto {\n      thumbnailUrl\n      id\n    }\n    review {\n      score\n      description\n      count\n    }\n  }\n}\n\nfragment Address_address on Address {\n  street\n  city\n  zip\n}\n",
+  "text": "query SingleHotelMapScreenQuery(\n  $search: AvailableHotelSearchInput!\n  $options: AvailableHotelOptionsInput\n) {\n  availableHotel(search: $search, options: $options) {\n    hotel {\n      ...MapView_hotel\n      id\n    }\n    ...AdditionalInfo\n    id\n  }\n}\n\nfragment MapView_hotel on Hotel {\n  coordinates {\n    lat\n    lng\n  }\n}\n\nfragment AdditionalInfo on HotelAvailability {\n  ...HotelDetailPreview_availability\n  hotel {\n    address {\n      ...Address_address\n    }\n    id\n  }\n}\n\nfragment HotelDetailPreview_availability on HotelAvailability {\n  price {\n    amount\n    currency\n  }\n  hotel {\n    ...HotelReviewScore_hotel\n    id\n    name\n    mainPhoto {\n      thumbnailUrl\n      id\n    }\n    rating {\n      stars\n    }\n  }\n}\n\nfragment Address_address on Address {\n  street\n  city\n  zip\n}\n\nfragment HotelReviewScore_hotel on Hotel {\n  review {\n    score\n  }\n}\n",
   "metadata": {},
   "fragment": {
     "kind": "Fragment",
@@ -241,6 +246,24 @@ return {
                 ]
               },
               v2,
+              {
+                "kind": "LinkedField",
+                "alias": null,
+                "name": "review",
+                "storageKey": null,
+                "args": null,
+                "concreteType": "HotelReview",
+                "plural": false,
+                "selections": [
+                  {
+                    "kind": "ScalarField",
+                    "alias": null,
+                    "name": "score",
+                    "args": null,
+                    "storageKey": null
+                  }
+                ]
+              },
               {
                 "kind": "ScalarField",
                 "alias": null,
@@ -270,30 +293,16 @@ return {
               {
                 "kind": "LinkedField",
                 "alias": null,
-                "name": "review",
+                "name": "rating",
                 "storageKey": null,
                 "args": null,
-                "concreteType": "HotelReview",
+                "concreteType": "HotelRating",
                 "plural": false,
                 "selections": [
                   {
                     "kind": "ScalarField",
                     "alias": null,
-                    "name": "score",
-                    "args": null,
-                    "storageKey": null
-                  },
-                  {
-                    "kind": "ScalarField",
-                    "alias": null,
-                    "name": "description",
-                    "args": null,
-                    "storageKey": null
-                  },
-                  {
-                    "kind": "ScalarField",
-                    "alias": null,
-                    "name": "count",
+                    "name": "stars",
                     "args": null,
                     "storageKey": null
                   }
diff --git a/app/hotels/src/navigation/allHotels/SearchResultsScreen.js b/app/hotels/src/navigation/allHotels/SearchResultsScreen.js
index c6eb6501..b3f86842 100644
--- a/app/hotels/src/navigation/allHotels/SearchResultsScreen.js
+++ b/app/hotels/src/navigation/allHotels/SearchResultsScreen.js
@@ -10,9 +10,8 @@ import {
   GestureController,
   TextIcon,
   CloseButton,
+  Device,
 } from '@kiwicom/mobile-shared';
-import { defaultTokens } from '@kiwicom/mobile-orbit';
-import { SafeAreaView } from 'react-navigation';
 
 import NewAllHotels from '../../allHotels/NewAllHotels';
 import MapHeaderButton from './MapHeaderButton';
@@ -110,14 +109,12 @@ class SearchResultsScreen extends React.Component<Props> {
     >
       <LayoutDoubleColumn
         menuComponent={
-          <SafeAreaView style={[styles.container, styles.safeArea]}>
-            <View style={styles.container}>
-              <NewAllHotels />
-              <View style={styles.button}>
-                <CloseButton onPress={this.onClosePress} />
-              </View>
+          <View style={styles.container}>
+            <NewAllHotels />
+            <View style={styles.button}>
+              <CloseButton onPress={this.onClosePress} />
             </View>
-          </SafeAreaView>
+          </View>
         }
         containerComponent={<SingleHotelContainer goBack={noop} />}
       />
@@ -126,15 +123,12 @@ class SearchResultsScreen extends React.Component<Props> {
 }
 
 const styles = StyleSheet.create({
-  safeArea: {
-    backgroundColor: defaultTokens.paletteWhite,
-  },
   container: {
     flex: 1,
   },
   button: {
     position: 'absolute',
-    bottom: 8,
+    bottom: Device.isIPhoneX ? 36 : 8,
     start: 8,
     end: 8,
   },
diff --git a/app/hotels/src/singleHotel/hotelInformation/HotelReview.js b/app/hotels/src/singleHotel/hotelInformation/HotelReview.js
index 1ea25358..ab2e2123 100644
--- a/app/hotels/src/singleHotel/hotelInformation/HotelReview.js
+++ b/app/hotels/src/singleHotel/hotelInformation/HotelReview.js
@@ -8,7 +8,7 @@ import { defaultTokens } from '@kiwicom/mobile-orbit';
 import { Translation } from '@kiwicom/mobile-localization';
 import idx from 'idx';
 
-import HotelReviewScore from '../../allHotels/HotelReviewScore';
+import HotelReviewScore from '../../components/HotelReviewScore';
 import type { HotelReview as HotelReviewType } from './__generated__/HotelReview.graphql';
 
 type Props = {|
diff --git a/packages/shared/src/BottomSheet.js b/packages/shared/src/BottomSheet.js
index 00196e7e..ac73c216 100644
--- a/packages/shared/src/BottomSheet.js
+++ b/packages/shared/src/BottomSheet.js
@@ -6,6 +6,7 @@ import { defaultTokens } from '@kiwicom/mobile-orbit';
 
 import StyleSheet from './PlatformStyleSheet';
 import VerticalSwipeResponder from './view/VerticalSwipeResponder';
+import Color from './Color';
 
 type Props = {|
   +children: React.Node,
@@ -54,6 +55,7 @@ const styles = StyleSheet.create({
   container: {
     backgroundColor: defaultTokens.paletteWhite,
     overflow: 'hidden',
+    borderRadius: 6,
     android: {
       borderTopRightRadius: 3,
       borderTopLeftRadius: 3,
@@ -62,5 +64,14 @@ const styles = StyleSheet.create({
       marginEnd: 1,
       elevation: 1,
     },
+    ios: {
+      shadowColor: Color.dark.$10,
+      shadowOffset: {
+        width: 0,
+        height: 2,
+      },
+      shadowOpacity: 0.1,
+      shadowRadius: 4,
+    },
   },
 });
diff --git a/packages/shared/src/Device.js b/packages/shared/src/Device.js
index abe86098..bd7210e6 100644
--- a/packages/shared/src/Device.js
+++ b/packages/shared/src/Device.js
@@ -1,8 +1,30 @@
 // @flow strict
 
-import { Platform } from 'react-native';
+import { Platform, Dimensions } from 'react-native';
 import { type DimensionType } from '@kiwicom/mobile-shared';
 
+// Borrowed this code from https://github.com/react-community/react-native-safe-area-view
+const X_WIDTH = 375;
+const X_HEIGHT = 812;
+const XSMAX_WIDTH = 414;
+const XSMAX_HEIGHT = 896;
+
+const { height: D_HEIGHT, width: D_WIDTH } = Dimensions.get('window');
+
+const isIPhoneX = (() => {
+  if (Platform.OS === 'web') {
+    return false;
+  }
+
+  return (
+    (Platform.OS === 'ios' &&
+      ((D_HEIGHT === X_HEIGHT && D_WIDTH === X_WIDTH) ||
+        (D_HEIGHT === X_WIDTH && D_WIDTH === X_HEIGHT))) ||
+    ((D_HEIGHT === XSMAX_HEIGHT && D_WIDTH === XSMAX_WIDTH) ||
+      (D_HEIGHT === XSMAX_WIDTH && D_WIDTH === XSMAX_HEIGHT))
+  );
+})();
+
 export default {
   /**
    * .---.
@@ -48,4 +70,6 @@ export default {
     android: 56,
     ios: 64,
   }),
+
+  isIPhoneX,
 };
diff --git a/packages/shared/src/Stars.js b/packages/shared/src/Stars.js
index 0a2f2b43..0577554c 100644
--- a/packages/shared/src/Stars.js
+++ b/packages/shared/src/Stars.js
@@ -5,15 +5,17 @@ import { Translation } from '@kiwicom/mobile-localization';
 
 import Text from './Text';
 import StyleSheet from './PlatformStyleSheet';
+import type { StylePropType } from '../index';
 
 type Props = {|
   // number of stars
   +rating?: ?number,
+  +style?: StylePropType,
 |};
 
-export default function Stars({ rating }: Props) {
+export default function Stars({ rating, style }: Props) {
   return (
-    <Text style={styles.stars}>
+    <Text style={[styles.stars, style]}>
       <Translation passThrough={'★'.repeat(rating || 0)} />
     </Text>
   );


Test patch:
diff --git a/packages/shared/src/__tests__/__snapshots__/Stars.test.js.snap b/packages/shared/src/__tests__/__snapshots__/Stars.test.js.snap
index 691312ed..94d6953a 100644
--- a/packages/shared/src/__tests__/__snapshots__/Stars.test.js.snap
+++ b/packages/shared/src/__tests__/__snapshots__/Stars.test.js.snap
@@ -3,9 +3,12 @@
 exports[`works with failed rating prop 1`] = `
 <Text
   style={
-    Object {
-      "letterSpacing": 2,
-    }
+    Array [
+      Object {
+        "letterSpacing": 2,
+      },
+      undefined,
+    ]
   }
 >
   <Translation
@@ -17,9 +20,12 @@ exports[`works with failed rating prop 1`] = `
 exports[`works with failed rating prop 2`] = `
 <Text
   style={
-    Object {
-      "letterSpacing": 2,
-    }
+    Array [
+      Object {
+        "letterSpacing": 2,
+      },
+      undefined,
+    ]
   }
 >
   <Translation
@@ -31,9 +37,12 @@ exports[`works with failed rating prop 2`] = `
 exports[`works with rating prop 1`] = `
 <Text
   style={
-    Object {
-      "letterSpacing": 2,
-    }
+    Array [
+      Object {
+        "letterSpacing": 2,
+      },
+      undefined,
+    ]
   }
 >
   <Translation
@@ -45,9 +54,12 @@ exports[`works with rating prop 1`] = `
 exports[`works with rating prop 2`] = `
 <Text
   style={
-    Object {
-      "letterSpacing": 2,
-    }
+    Array [
+      Object {
+        "letterSpacing": 2,
+      },
+      undefined,
+    ]
   }
 >
   <Translation