• Экраны
  • Доки
1Online
ПодпискаКаталог
  • Экраны
  • Доки
1Online
ПодпискаКаталог
  • Экраны
  • Доки
1Online
ПодпискаКаталог
Настройки профиля V4 — экран из раздела «Настройки»
Настройки

Настройки профиля V4

Экраны профиля и настроек из сгруппированных строк.

Установка

src/shared/components/blocks/profile-settings-v4
npx astra-rn add profile-settings-v4
pnpm dlx astra-rn add profile-settings-v4
yarn dlx astra-rn add profile-settings-v4
bunx --bun astra-rn add profile-settings-v4

Зависимости

6 пакетов
npm install @hugeicons/core-free-icons @hugeicons/react-native @shopify/react-native-skia expo-asset react-native-reanimated react-native-safe-area-context
pnpm add @hugeicons/core-free-icons @hugeicons/react-native @shopify/react-native-skia expo-asset react-native-reanimated react-native-safe-area-context
yarn add @hugeicons/core-free-icons @hugeicons/react-native @shopify/react-native-skia expo-asset react-native-reanimated react-native-safe-area-context
bun add @hugeicons/core-free-icons @hugeicons/react-native @shopify/react-native-skia expo-asset react-native-reanimated react-native-safe-area-context

Исходник

4 файла
import {
  ArrowRight01Icon,
  Delete02Icon,
  Logout03Icon,
  PencilEdit02Icon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react-native";
import React, { memo, useCallback, useState } from "react";
import {
  Image,
  Pressable,
  ScrollView,
  StyleSheet,
  Text,
  View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { SquircleView } from "@/shared/components/base/squircle-view";
import { Switch } from "@/shared/components/primitives/switch";
import {
  AVATAR_SIZE,
  CARD_BORDER_WIDTH,
  CARD_CORNER_SMOOTHING,
  CARD_RADIUS,
  COLORS,
  CONTENT_BOTTOM_PADDING,
  CONTENT_HORIZONTAL_PADDING,
  CONTENT_TOP_PADDING,
  DEFAULT_DELETE_LABEL,
  DEFAULT_LOGOUT_LABEL,
  DEFAULT_PROFILE,
  DEFAULT_SECTIONS,
  DEFAULT_SUBTITLE,
  DEFAULT_TITLE,
  DEFAULT_VERSION_LABEL,
  DELETE_GAP,
  DELETE_HEIGHT,
  DELETE_MARGIN_TOP,
  DELETE_RADIUS,
  EDIT_SIZE,
  FONT_FAMILY,
  HEADER_MARGIN_BOTTOM,
  LABEL_GAP,
  LOGOUT_GAP,
  LOGOUT_HEIGHT,
  LOGOUT_MARGIN_TOP,
  LOGOUT_RADIUS,
  PROFILE_GAP,
  PROFILE_PADDING,
  ROW_HEIGHT,
  ROW_HORIZONTAL_PADDING,
  ROW_ICON_GAP,
  ROW_ICON_SIZE,
  SECTION_GAP,
  SEPARATOR_INSET,
  SUBTITLE_MARGIN_TOP,
} from "./const";
import type {
  IProfileSettingsV4,
  ISettingsRow,
  ISettingsRowItem,
} from "./types";

const SettingsRowItem: React.FC<ISettingsRowItem> = memo(
  ({
    id,
    title,
    icon,
    accessory = "chevron",
    value,
    isLast,
    checked = false,
    onPress,
    onToggle,
  }: ISettingsRowItem) => {
    const [isPressed, setIsPressed] = useState<boolean>(false);

    const handlePressIn = useCallback((): void => setIsPressed(true), []);
    const handlePressOut = useCallback((): void => setIsPressed(false), []);
    const handlePress = useCallback((): void => onPress?.(id), [id, onPress]);
    const handleCheckedChange = useCallback(
      (next: boolean): void => onToggle?.(id, next),
      [id, onToggle],
    );

    const isSwitch = accessory === "switch";

    return (
      <Pressable
        accessibilityRole={isSwitch ? "switch" : "button"}
        accessibilityLabel={title}
        accessibilityState={isSwitch ? { checked } : undefined}
        disabled={isSwitch}
        onPress={handlePress}
        onPressIn={handlePressIn}
        onPressOut={handlePressOut}
        style={[styles.row, isPressed ? styles.rowPressed : null]}
      >
        <View style={styles.rowIcon}>
          <HugeiconsIcon icon={icon} size={ROW_ICON_SIZE} color={COLORS.icon} />
        </View>

        <Text style={styles.rowTitle} numberOfLines={1}>
          {title}
        </Text>

        {accessory === "value" && value ? (
          <Text style={styles.rowValue} numberOfLines={1}>
            {value}
          </Text>
        ) : null}

        {isSwitch ? (
          <Switch.Root
            size="sm"
            checked={checked}
            trackColor={COLORS.switchTrack}
            onCheckedChange={handleCheckedChange}
          >
            <Switch.Track>
              <Switch.Thumb />
            </Switch.Track>
          </Switch.Root>
        ) : accessory === "none" ? null : (
          <HugeiconsIcon
            icon={ArrowRight01Icon}
            size={17}
            color={COLORS.accessory}
          />
        )}

        {isLast ? null : <View style={styles.separator} />}
      </Pressable>
    );
  },
);

const buildToggleState = (
  sections: IProfileSettingsV4["sections"],
): Record<string, boolean> => {
  const state: Record<string, boolean> = {};

  sections?.forEach((section) =>
    section.rows.forEach((row: ISettingsRow) => {
      if (row.accessory === "switch")
        state[row.id] = row.defaultChecked ?? false;
    }),
  );

  return state;
};

const ProfileSettingsV4: React.FC<IProfileSettingsV4> = ({
  title = DEFAULT_TITLE,
  subtitle = DEFAULT_SUBTITLE,
  profile = DEFAULT_PROFILE,
  sections = DEFAULT_SECTIONS,
  logoutLabel = DEFAULT_LOGOUT_LABEL,
  deleteLabel = DEFAULT_DELETE_LABEL,
  versionLabel = DEFAULT_VERSION_LABEL,
  hideEdit = false,
  hideDelete = false,
  style,
  onEditPress,
  onRowPress,
  onRowToggle,
  onLogoutPress,
  onDeletePress,
}: IProfileSettingsV4) => {
  const [toggles, setToggles] = useState<Record<string, boolean>>(() =>
    buildToggleState(sections),
  );

  const handleRowToggle = useCallback(
    (id: string, checked: boolean): void => {
      setToggles((prev) => ({ ...prev, [id]: checked }));
      onRowToggle?.(id, checked);
    },
    [onRowToggle],
  );

  const insets = useSafeAreaInsets();

  return (
    <View style={[styles.container, style]}>
      <ScrollView
        contentContainerStyle={[
          styles.content,
          { paddingTop: insets.top + CONTENT_TOP_PADDING + insets.bottom },
        ]}
        showsVerticalScrollIndicator={false}
      >
        <View style={styles.header}>
          <Text style={styles.title} numberOfLines={1}>
            {title}
          </Text>
          {subtitle ? (
            <Text style={styles.subtitle} numberOfLines={2}>
              {subtitle}
            </Text>
          ) : null}
        </View>

        <SquircleView
          cornerRadius={CARD_RADIUS}
          cornerSmoothing={CARD_CORNER_SMOOTHING}
          backgroundColor={COLORS.card}
          borderWidth={CARD_BORDER_WIDTH}
        >
          <View style={styles.profile}>
            {profile.avatar ? (
              <Image
                source={profile.avatar}
                style={styles.avatar}
                resizeMode="cover"
                accessibilityIgnoresInvertColors
              />
            ) : (
              <View style={[styles.avatar, styles.avatarPlaceholder]} />
            )}

            <View style={styles.profileText}>
              <Text style={styles.name} numberOfLines={1}>
                {profile.name}
              </Text>
              {profile.email ? (
                <Text style={styles.email} numberOfLines={1}>
                  {profile.email}
                </Text>
              ) : null}
            </View>

            {hideEdit ? null : (
              <Pressable
                accessibilityRole="button"
                accessibilityLabel="Edit profile"
                onPress={onEditPress}
                style={({ pressed }) => [
                  styles.edit,
                  pressed ? styles.pressedSubtle : null,
                ]}
              >
                <HugeiconsIcon
                  icon={PencilEdit02Icon}
                  size={17}
                  color={COLORS.editIcon}
                />
              </Pressable>
            )}
          </View>
        </SquircleView>

        {sections.map((section) => (
          <View key={section.id} style={styles.section}>
            {section.label ? (
              <Text style={styles.label}>{section.label}</Text>
            ) : null}

            <SquircleView
              cornerRadius={CARD_RADIUS}
              cornerSmoothing={CARD_CORNER_SMOOTHING}
              backgroundColor={COLORS.card}
              borderWidth={CARD_BORDER_WIDTH}
            >
              {section.rows.map((row, index) => (
                <SettingsRowItem
                  key={row.id}
                  {...row}
                  isLast={index === section.rows.length - 1}
                  checked={toggles[row.id]}
                  onPress={onRowPress}
                  onToggle={handleRowToggle}
                />
              ))}
            </SquircleView>
          </View>
        ))}

        <Pressable
          accessibilityRole="button"
          accessibilityLabel={logoutLabel}
          onPress={onLogoutPress}
          style={styles.logout}
        >
          <HugeiconsIcon
            icon={Logout03Icon}
            size={19}
            color={COLORS.logoutLabel}
          />
          <Text style={styles.logoutLabel} numberOfLines={1}>
            {logoutLabel}
          </Text>
        </Pressable>

        {hideDelete ? null : (
          <Pressable
            accessibilityRole="button"
            accessibilityLabel={deleteLabel}
            onPress={onDeletePress}
            style={styles.deleteButton}
          >
            <HugeiconsIcon
              icon={Delete02Icon}
              size={19}
              color={COLORS.deleteLabel}
            />
            <Text style={styles.deleteLabel} numberOfLines={1}>
              {deleteLabel}
            </Text>
          </Pressable>
        )}

        {versionLabel ? (
          <Text style={styles.version}>{versionLabel}</Text>
        ) : null}
      </ScrollView>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: COLORS.screen,
  },
  pressedSubtle: {
    opacity: 0.55,
  },
  content: {
    paddingHorizontal: CONTENT_HORIZONTAL_PADDING,
    paddingBottom: CONTENT_BOTTOM_PADDING,
  },
  header: {
    marginBottom: HEADER_MARGIN_BOTTOM,
  },
  title: {
    fontFamily: FONT_FAMILY,
    fontSize: 34,
    fontWeight: "700",
    letterSpacing: -0.9,
    color: COLORS.title,
  },
  subtitle: {
    marginTop: SUBTITLE_MARGIN_TOP,
    fontFamily: FONT_FAMILY,
    fontSize: 13.5,
    fontWeight: "400",
    letterSpacing: -0.1,
    color: COLORS.subtitle,
  },
  profile: {
    flexDirection: "row",
    alignItems: "center",
    width: "100%",
    padding: PROFILE_PADDING,
  },
  avatar: {
    width: AVATAR_SIZE,
    height: AVATAR_SIZE,
    borderRadius: AVATAR_SIZE / 2,
  },
  avatarPlaceholder: {
    backgroundColor: COLORS.avatar,
  },
  profileText: {
    flex: 1,
    minWidth: 0,
    marginLeft: PROFILE_GAP,
  },
  name: {
    fontFamily: FONT_FAMILY,
    fontSize: 17.5,
    fontWeight: "600",
    letterSpacing: -0.4,
    color: COLORS.name,
  },
  email: {
    marginTop: 3,
    fontFamily: FONT_FAMILY,
    fontSize: 13.5,
    fontWeight: "400",
    letterSpacing: -0.1,
    color: COLORS.email,
  },
  edit: {
    alignItems: "center",
    justifyContent: "center",
    width: EDIT_SIZE,
    height: EDIT_SIZE,
    borderRadius: EDIT_SIZE / 2,
    backgroundColor: COLORS.edit,
  },
  section: {
    marginTop: SECTION_GAP,
  },
  label: {
    marginBottom: LABEL_GAP,
    marginLeft: 4,
    fontFamily: FONT_FAMILY,
    fontSize: 13,
    fontWeight: "600",
    letterSpacing: -0.1,
    color: COLORS.label,
  },
  row: {
    flexDirection: "row",
    alignItems: "center",
    height: ROW_HEIGHT,
    paddingHorizontal: ROW_HORIZONTAL_PADDING,
  },
  rowPressed: {
    backgroundColor: COLORS.cardPressed,
  },
  rowIcon: {
    width: ROW_ICON_SIZE,
    height: ROW_ICON_SIZE,
    marginRight: ROW_ICON_GAP,
    alignItems: "center",
    justifyContent: "center",
  },
  rowTitle: {
    flex: 1,
    fontFamily: FONT_FAMILY,
    fontSize: 16,
    fontWeight: "400",
    letterSpacing: -0.3,
    color: COLORS.rowTitle,
  },
  rowValue: {
    marginRight: 6,
    fontFamily: FONT_FAMILY,
    fontSize: 15.5,
    fontWeight: "400",
    letterSpacing: -0.2,
    color: COLORS.value,
  },
  separator: {
    position: "absolute",
    left: SEPARATOR_INSET,
    right: 0,
    bottom: 0,
    height: StyleSheet.hairlineWidth,
    backgroundColor: COLORS.separator,
  },
  logout: {
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "center",
    gap: LOGOUT_GAP,
    width: "100%",
    height: LOGOUT_HEIGHT,
    marginTop: LOGOUT_MARGIN_TOP,
    borderRadius: LOGOUT_RADIUS,
    backgroundColor: COLORS.logout,
  },
  logoutPressed: {
    backgroundColor: COLORS.logoutPressed,
  },
  logoutLabel: {
    fontFamily: FONT_FAMILY,
    fontSize: 16.5,
    fontWeight: "600",
    letterSpacing: -0.3,
    color: COLORS.logoutLabel,
  },
  deleteButton: {
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "center",
    gap: DELETE_GAP,
    width: "100%",
    height: DELETE_HEIGHT,
    marginTop: DELETE_MARGIN_TOP,
    borderRadius: DELETE_RADIUS,
    backgroundColor: COLORS.deleteButton,
  },
  deleteButtonPressed: {
    backgroundColor: COLORS.deleteButtonPressed,
  },
  deleteLabel: {
    fontFamily: FONT_FAMILY,
    fontSize: 16.5,
    fontWeight: "600",
    letterSpacing: -0.3,
    color: COLORS.deleteLabel,
  },
  version: {
    marginTop: CONTENT_TOP_PADDING * 2,
    textAlign: "center",
    fontFamily: FONT_FAMILY,
    fontSize: 12,
    fontWeight: "400",
    color: COLORS.version,
  },
});

export { ProfileSettingsV4 };
export type {
  IGlyph,
  IProfileSettingsV4,
  ISettingsProfile,
  ISettingsRow,
  ISettingsRowItem,
  ISettingsSection,
  TSettingsRowAccessory,
} from "./types";
export default memo(ProfileSettingsV4);
import {
  CreditCardIcon,
  FingerAccessIcon,
  HelpCircleIcon,
  Mail01Icon,
  Moon02Icon,
  Notification03Icon,
  Shield01Icon,
  SquareLock01Icon,
  TranslateIcon,
  UserCircleIcon,
} from "@hugeicons/core-free-icons";
import { Platform } from "react-native";
import type { ISettingsProfile, ISettingsSection } from "./types";
import { PHOTO_SOURCES } from "./photos";

export const FONT_FAMILY = Platform.select({
  ios: "SF Pro Text",
  default: undefined,
});

export const DEFAULT_TITLE = "Profile";
export const DEFAULT_SUBTITLE = "Manage your account and preferences";
export const DEFAULT_LOGOUT_LABEL = "Log Out";
export const DEFAULT_DELETE_LABEL = "Delete Account";
export const DEFAULT_VERSION_LABEL = "Version 1.0.0";

export const DEFAULT_PROFILE: ISettingsProfile = {
  name: "Ava Bennett",
  email: "avabennett@gmail.com",
  avatar: {
    uri: PHOTO_SOURCES["profile-settings-v4-1"],
  },
};

export const DEFAULT_SECTIONS: ISettingsSection[] = [
  {
    id: "account",
    label: "Account",
    rows: [
      {
        id: "personal-details",
        title: "Personal Details",
        icon: UserCircleIcon,
      },
      {
        id: "notifications",
        title: "Notifications",
        icon: Notification03Icon,
      },
      {
        id: "password-security",
        title: "Password & Security",
        icon: SquareLock01Icon,
      },
      {
        id: "payment-methods",
        title: "Payment Methods",
        icon: CreditCardIcon,
      },
    ],
  },
  {
    id: "preferences",
    label: "Preferences",
    rows: [
      {
        id: "appearance",
        title: "Appearance",
        icon: Moon02Icon,
        accessory: "value",
        value: "Light",
      },
      {
        id: "language",
        title: "Language",
        icon: TranslateIcon,
        accessory: "value",
        value: "English",
      },
      {
        id: "face-id",
        title: "Unlock with Face ID",
        icon: FingerAccessIcon,
        accessory: "switch",
        defaultChecked: true,
      },
    ],
  },
  {
    id: "support",
    label: "Support",
    rows: [
      {
        id: "help-center",
        title: "Help Center",
        icon: HelpCircleIcon,
      },
      {
        id: "privacy-policy",
        title: "Privacy Policy",
        icon: Shield01Icon,
      },
      {
        id: "contact-us",
        title: "Contact Us",
        icon: Mail01Icon,
      },
    ],
  },
];

export const CONTENT_HORIZONTAL_PADDING = 18;
export const CONTENT_TOP_PADDING = 10;
export const CONTENT_BOTTOM_PADDING = 32;
export const SUBTITLE_MARGIN_TOP = 4;
export const HEADER_MARGIN_BOTTOM = 20;
export const CARD_RADIUS = 22;
export const CARD_CORNER_SMOOTHING = 2;
export const CARD_BORDER_WIDTH = 0;
export const PROFILE_PADDING = 16;
export const PROFILE_GAP = 14;
export const AVATAR_SIZE = 65;
export const EDIT_SIZE = 34;
export const ROW_HEIGHT = 50;
export const ROW_HORIZONTAL_PADDING = 16;
export const ROW_ICON_SIZE = 19;
export const ROW_ICON_GAP = 13;
export const SEPARATOR_INSET =
  ROW_HORIZONTAL_PADDING + ROW_ICON_SIZE + ROW_ICON_GAP;
export const SECTION_GAP = 26;
export const LABEL_GAP = 9;
export const LOGOUT_MARGIN_TOP = 26;
export const LOGOUT_HEIGHT = 54;
export const LOGOUT_RADIUS = LOGOUT_HEIGHT / 2;
export const LOGOUT_GAP = 9;
export const DELETE_MARGIN_TOP = 10;
export const DELETE_HEIGHT = LOGOUT_HEIGHT;
export const DELETE_RADIUS = DELETE_HEIGHT / 2;
export const DELETE_GAP = LOGOUT_GAP;

export const COLORS = {
  screen: "#f2f2f7",
  title: "#000000",
  subtitle: "#8a8a8e",
  card: "#ffffff",
  cardPressed: "#f0f0f4",
  avatar: "#e3e3e8",
  name: "#000000",
  email: "#8a8a8e",
  edit: "rgba(120,120,128,0.12)",
  editIcon: "#111114",
  label: "#6d6d73",
  rowTitle: "#111114",
  icon: "#1c1c1e",
  value: "#8a8a8e",
  accessory: "#c4c4c8",
  separator: "rgba(60,60,67,0.13)",
  switchTrack: "#34c759",
  logout: "#0a0a0c",
  logoutPressed: "#2c2c30",
  logoutLabel: "#ffffff",
  deleteButton: "#ff5555",
  deleteButtonPressed: "#f0f0f4",
  deleteLabel: "#ffeceb",
  version: "#a0a0a6",
} as const;
import { IconSvgElement } from "@hugeicons/react-native";
import type { ImageSourcePropType, StyleProp, ViewStyle } from "react-native";

export interface IGlyph {
  size?: number;
  color?: string;
}

export type TSettingsRowAccessory = "chevron" | "value" | "switch" | "none";

export interface ISettingsRow {
  id: string;
  title: string;
  icon: IconSvgElement;
  accessory?: TSettingsRowAccessory;
  value?: string;
  defaultChecked?: boolean;
}

export interface ISettingsSection {
  id: string;
  label?: string;
  rows: ISettingsRow[];
}

export interface ISettingsProfile {
  name: string;
  email?: string;
  avatar?: ImageSourcePropType;
}

export interface ISettingsRowItem extends ISettingsRow {
  isLast: boolean;
  checked?: boolean;
  onPress?: (id: string) => void;
  onToggle?: (id: string, checked: boolean) => void;
}

export interface IProfileSettingsV4 {
  title?: string;
  subtitle?: string;
  profile?: ISettingsProfile;
  sections?: ISettingsSection[];
  logoutLabel?: string;
  deleteLabel?: string;
  versionLabel?: string;
  hideEdit?: boolean;
  hideDelete?: boolean;
  style?: StyleProp<ViewStyle>;
  onEditPress?: () => void;
  onRowPress?: (id: string) => void;
  onRowToggle?: (id: string, checked: boolean) => void;
  onLogoutPress?: () => void;
  onDeletePress?: () => void;
}
import { Asset } from "expo-asset";

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

export const PHOTO_SOURCES = {
  "profile-settings-v4-1": uri(require("./profile-settings-v4-1.webp")),
};

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

  • src/shared/components/blocks/profile-settings-v4/photos/profile-settings-v4-1.webp42 КБСкачать

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

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

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