• Экраны
  • Доки
1Online
ПодпискаКаталог
  • Экраны
  • Доки
1Online
ПодпискаКаталог
  • Экраны
  • Доки
1Online
ПодпискаКаталог
Промо Airbnb — экран из раздела «Шторка»
Шторка

Промо Airbnb

Шторки, всплывающие над затемнением — формы, акции, оплата и вход.

Установка

src/shared/components/blocks/airbnb-v1
npx astra-rn add airbnb-v1
pnpm dlx astra-rn add airbnb-v1
yarn dlx astra-rn add airbnb-v1
bunx --bun astra-rn add airbnb-v1

Зависимости

5 пакетов
npm install @gorhom/bottom-sheet expo-asset expo-router react-native-safe-area-context react-native-svg
pnpm add @gorhom/bottom-sheet expo-asset expo-router react-native-safe-area-context react-native-svg
yarn add @gorhom/bottom-sheet expo-asset expo-router react-native-safe-area-context react-native-svg
bun add @gorhom/bottom-sheet expo-asset expo-router react-native-safe-area-context react-native-svg

Исходник

4 файла
import React, { memo, useCallback, useMemo, useRef, useState } from "react";
import { Image, Linking, Pressable, StyleSheet, Text } from "react-native";
import { useFocusEffect } from "expo-router";
import BottomSheet, {
  BottomSheetBackdrop,
  BottomSheetView,
  type BottomSheetBackdropProps,
} from "@gorhom/bottom-sheet";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Svg, { Path } from "react-native-svg";
import type { IAirBnbBottomSheetDiscount, ICloseIcon } from "./types";
import {
  BACKDROP_OPACITY,
  COLORS,
  CONTENT_BOTTOM_PADDING,
  DEFAULT_ACTION_LABEL,
  DEFAULT_ARTWORK_URI,
  DEFAULT_DESCRIPTION,
  DEFAULT_FOOTNOTE,
  DEFAULT_FOOTNOTE_LINK_LABEL,
  DEFAULT_FOOTNOTE_LINK_URL,
  DEFAULT_TITLE,
  SHEET_CORNER_RADIUS,
} from "./const";

const CloseIcon: React.FC<ICloseIcon> = ({
  size = 18,
  color = COLORS.ink,
}: ICloseIcon) => (
  <Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
    <Path
      d="M6 6L18 18M18 6L6 18"
      stroke={color}
      strokeWidth={2.2}
      strokeLinecap="round"
    />
  </Svg>
);

const AirBnbBottomSheetDiscount: React.FC<IAirBnbBottomSheetDiscount> = ({
  title = DEFAULT_TITLE,
  description = DEFAULT_DESCRIPTION,
  actionLabel = DEFAULT_ACTION_LABEL,
  footnote = DEFAULT_FOOTNOTE,
  footnoteLinkLabel = DEFAULT_FOOTNOTE_LINK_LABEL,
  footnoteLinkUrl = DEFAULT_FOOTNOTE_LINK_URL,
  artwork = { uri: DEFAULT_ARTWORK_URI },
  openOnFocus = true,
  onActionPress,
  onClose,
}: IAirBnbBottomSheetDiscount) => {
  const sheetRef = useRef<BottomSheet>(null);
  const insets = useSafeAreaInsets();
  const [isMounted, setIsMounted] = useState<boolean>(openOnFocus);
  const [isClosePressed, setIsClosePressed] = useState<boolean>(false);
  const [isActionPressed, setIsActionPressed] = useState<boolean>(false);

  useFocusEffect(
    useCallback(() => {
      if (openOnFocus) setIsMounted(true);
      return () => {
        sheetRef.current?.close();
        setIsMounted(false);
      };
    }, [openOnFocus]),
  );

  const handleClose = useCallback((): void => {
    sheetRef.current?.close();
  }, []);

  const handleChange = useCallback(
    (index: number): void => {
      if (index !== -1) return;
      setIsMounted(false);
      onClose?.();
    },
    [onClose],
  );

  const handleClosePressIn = useCallback(
    (): void => setIsClosePressed(true),
    [],
  );
  const handleClosePressOut = useCallback(
    (): void => setIsClosePressed(false),
    [],
  );

  const handleActionPressIn = useCallback(
    (): void => setIsActionPressed(true),
    [],
  );
  const handleActionPressOut = useCallback(
    (): void => setIsActionPressed(false),
    [],
  );

  const handleFootnoteLinkPress = useCallback((): void => {
    if (footnoteLinkUrl) void Linking.openURL(footnoteLinkUrl);
  }, [footnoteLinkUrl]);

  const renderBackdrop = useCallback(
    (props: BottomSheetBackdropProps) => (
      <BottomSheetBackdrop
        {...props}
        appearsOnIndex={0}
        disappearsOnIndex={-1}
        opacity={BACKDROP_OPACITY}
        pressBehavior="close"
      />
    ),
    [],
  );

  const contentStyle = useMemo(
    () => [
      styles.content,
      { paddingBottom: CONTENT_BOTTOM_PADDING + insets.bottom },
    ],
    [insets.bottom],
  );

  if (!isMounted) return null;

  return (
    <BottomSheet
      ref={sheetRef}
      index={0}
      enablePanDownToClose
      enableDynamicSizing
      onChange={handleChange}
      backdropComponent={renderBackdrop}
      handleComponent={null}
      backgroundStyle={styles.background}
      style={styles.sheet}
    >
      <BottomSheetView style={contentStyle}>
        <Pressable
          accessibilityRole="button"
          accessibilityLabel="Close"
          hitSlop={12}
          onPress={handleClose}
          onPressIn={handleClosePressIn}
          onPressOut={handleClosePressOut}
          style={[
            styles.closeButton,
            isClosePressed ? styles.closeButtonPressed : null,
          ]}
        >
          <CloseIcon />
        </Pressable>

        <Image source={artwork} style={styles.artwork} resizeMode="contain" />

        <Text style={styles.title}>{title}</Text>
        <Text style={styles.description}>{description}</Text>

        <Pressable
          accessibilityRole="button"
          accessibilityLabel={actionLabel}
          onPress={onActionPress}
          onPressIn={handleActionPressIn}
          onPressOut={handleActionPressOut}
          style={[styles.action, isActionPressed ? styles.actionPressed : null]}
        >
          <Text style={styles.actionLabel} numberOfLines={1}>
            {actionLabel}
          </Text>
        </Pressable>

        <Text style={styles.footnote}>
          {footnote}
          {footnoteLinkLabel ? (
            <Text style={styles.footnoteLink} onPress={handleFootnoteLinkPress}>
              {footnoteLinkLabel}
            </Text>
          ) : null}
        </Text>
      </BottomSheetView>
    </BottomSheet>
  );
};

const styles = StyleSheet.create({
  sheet: {
    shadowColor: "#000000",
    shadowOpacity: 0.18,
    shadowRadius: 24,
    shadowOffset: { width: 0, height: -6 },
    elevation: 16,
  },
  background: {
    backgroundColor: COLORS.sheet,
    borderTopLeftRadius: SHEET_CORNER_RADIUS,
    borderTopRightRadius: SHEET_CORNER_RADIUS,
  },
  content: {
    alignSelf: "stretch",
    alignItems: "center",
    paddingHorizontal: 24,
    paddingTop: 20,
  },
  closeButton: {
    position: "absolute",
    top: 14,
    right: 16,
    zIndex: 10,
    width: 36,
    height: 36,
    borderRadius: 18,
    alignItems: "center",
    justifyContent: "center",
  },
  closeButtonPressed: {
    backgroundColor: COLORS.overlayPressed,
  },
  artwork: {
    width: 148,
    height: 148,
    marginTop: 24,
    marginBottom: 30,
  },
  title: {
    alignSelf: "stretch",
    maxWidth: 370,
    fontSize: 32,
    lineHeight: 38,
    fontWeight: "700",
    letterSpacing: -0.8,
    color: COLORS.ink,
    textAlign: "center",
  },
  description: {
    alignSelf: "stretch",
    marginTop: 16,
    fontSize: 16,
    lineHeight: 22,
    color: COLORS.muted,
    textAlign: "center",
  },
  action: {
    alignSelf: "stretch",
    marginTop: 28,
    height: 60,
    borderRadius: 16,
    backgroundColor: COLORS.action,
    alignItems: "center",
    justifyContent: "center",
    overflow: "hidden",
  },
  actionPressed: {
    backgroundColor: COLORS.actionPressed,
  },
  actionLabel: {
    fontSize: 17,
    fontWeight: "700",
    letterSpacing: -0.2,
    color: COLORS.ink,
  },
  footnote: {
    marginTop: 20,
    fontSize: 12,
    lineHeight: 16,
    color: COLORS.subtle,
    textAlign: "center",
  },
  footnoteLink: {
    color: COLORS.subtle,
    textDecorationLine: "underline",
  },
});

export { AirBnbBottomSheetDiscount };
export default memo(AirBnbBottomSheetDiscount);
import { PHOTO_SOURCES } from "./photos";

export const DEFAULT_ARTWORK_URI =
  PHOTO_SOURCES["airbnb-v1-1"];

export const DEFAULT_TITLE = "Get 15% in Airbnb credit at hotels";

export const DEFAULT_DESCRIPTION =
  "Stay at a featured hotel and get Airbnb credit to use on homes, experiences or services.";

export const DEFAULT_ACTION_LABEL = "Browse hotels";

export const DEFAULT_FOOTNOTE = "Excludes fees & taxes · Valid for 1 year · ";

export const DEFAULT_FOOTNOTE_LINK_LABEL = "Terms apply";

export const DEFAULT_FOOTNOTE_LINK_URL = "https://www.airbnb.com/terms";

export const SHEET_CORNER_RADIUS = 26;

export const BACKDROP_OPACITY = 0.45;

export const CONTENT_BOTTOM_PADDING = 24;

export const COLORS = {
  sheet: "#ffffff",
  ink: "#222222",
  muted: "#5c5c5c",
  subtle: "#767676",
  action: "#f2f2f2",
  actionPressed: "#e7e7e7",
  overlayPressed: "#f2f2f2",
} as const;
import type { ImageSourcePropType } from "react-native";

export interface IAirBnbBottomSheetDiscount {
  title?: string;

  description?: string;

  actionLabel?: string;

  footnote?: string;

  footnoteLinkLabel?: string;

  footnoteLinkUrl?: string;

  artwork?: ImageSourcePropType;

  openOnFocus?: boolean;

  onActionPress?: () => void;

  onClose?: () => void;
}

export interface ICloseIcon {
  size?: number;
  color?: string;
}
import { Asset } from "expo-asset";

const uri = (id: number): string => Asset.fromModule(id).uri;

export const PHOTO_SOURCES = {
  "airbnb-v1-1": uri(require("./airbnb-v1-1.webp")),
};

Ещё 1 файл с картинками или шрифтами. astra-rn add скачивает их сам, вручную положите их по этим путям:

  • src/shared/components/blocks/airbnb-v1/photos/airbnb-v1-1.webp215 КБСкачать

ПРИСОЕДИНЯЙТЕСЬ

ПолучитезначокAstra Proужесегодня

ASTRAPRO
Дизайн и разработка — ©Astra