Skip to content

[Doc]: Предлагаю вкладку в документации "ShowCases от комьюнити" это не компоненты, это fragments скорее #9602

@dsedinkin

Description

@dsedinkin

Описание

Код-ревью приветствуется. Тем кто будет предлагать какие-то свои кейсы. Вообще это должно в моменте закрыть какие-то типовые экраны, бутерброды-компонентов, которые как правило делаются достаточно часто, или их можно встретить в самом VK.

Тут наверное задача больше показать связки компонентов. Как они работают между собой. Если начать собирать и шлифовать какие-то типовые кейсы от комьюнити, можно прийти повышению качества мини-приложений и сайтов на vkui и сроками разработки c использованием по сути best-practices одобреными разработчиками vkui.

Примеры:

Адаптивный UI/UX выбора диапазона дат:

Image Image Image Image
import { useContext, useRef, useState } from "react";
import { AppContext } from "engine/state";

import {
  ActionSheet,
  ActionSheetItem,
  AppRootPortal,
  Button,
  CalendarRange,
  DateInput,
  Div,
  FormItem,
  ModalPage,
  ModalPageHeader,
  Popper,
  Separator,
} from "@vkontakte/vkui";
import { ModalPageHeaderButtonClose } from "engine/components";

import { Icon16DropdownOutline } from "@vkontakte/icons";

import "./ButtonSelectIntervalDate.css";

type AnalyticsIntervalKey =
  | "all"
  | "today"
  | "yesterday"
  | "last7"
  | "last30"
  | "custom";

type DateRangeValue = [Date | null, Date | null];

const DAY_IN_MS = 24 * 60 * 60 * 1000;

const INTERVALS: { key: AnalyticsIntervalKey; label: string }[] = [
  { key: "all", label: "За все время" },
  { key: "today", label: "Сегодня" },
  { key: "yesterday", label: "Вчера" },
  { key: "last7", label: "Последние 7 дней" },
  { key: "last30", label: "Последние 30 дней" },
  { key: "custom", label: "Указать период" },
];

const hasCompleteRange = (range?: DateRangeValue | null) =>
  Boolean(range?.[0] && range[1]);

const hasInvalidRangeOrder = (range?: DateRangeValue | null) =>
  Boolean(range?.[0] && range[1] && range[0] > range[1]);

const getStartOfDay = (date: Date) =>
  new Date(date.getFullYear(), date.getMonth(), date.getDate());

const isBeforeDay = (date: Date, compareDate: Date) =>
  getStartOfDay(date).getTime() < getStartOfDay(compareDate).getTime();

const isAfterDay = (date: Date, compareDate: Date) =>
  getStartOfDay(date).getTime() > getStartOfDay(compareDate).getTime();

const isFutureDay = (date: Date) => isAfterDay(date, new Date());

const shiftDays = (date: Date, amount: number) => {
  const nextDate = new Date(date);

  nextDate.setDate(nextDate.getDate() + amount);

  return getStartOfDay(nextDate);
};

const formatDate = (date: Date, showYear = false) => {
  if (showYear) {
    const dateParts = new Intl.DateTimeFormat("ru-RU", {
      day: "numeric",
      month: "short",
      year: "numeric",
    })
      .formatToParts(date)
      .reduce<Record<string, string>>((acc, part) => {
        if (part.type !== "literal") {
          acc[part.type] = part.value;
        }

        return acc;
      }, {});

    const day = dateParts.day ?? String(date.getDate());
    const month =
      dateParts.month?.replace(/\./g, "") ??
      new Intl.DateTimeFormat("ru-RU", { month: "short" })
        .format(date)
        .replace(/\./g, "");
    const year = dateParts.year ?? String(date.getFullYear());

    return `${day} ${month} ${year}`;
  }

  const day = String(date.getDate()).padStart(2, "0");
  const month = String(date.getMonth() + 1).padStart(2, "0");

  return `${day}.${month}`;
};

const isSameDay = (startDate: Date, endDate: Date) =>
  startDate.getFullYear() === endDate.getFullYear() &&
  startDate.getMonth() === endDate.getMonth() &&
  startDate.getDate() === endDate.getDate();

const formatRangeLabel = (
  range?: DateRangeValue | null,
  fallback = "Указать период",
) => {
  if (!range?.[0] || !range[1]) {
    return fallback;
  }

  const [startDate, endDate] = range;

  if (isSameDay(startDate, endDate)) {
    return formatDate(startDate);
  }

  const diffInDays = Math.floor(
    Math.abs(
      getStartOfDay(endDate).getTime() - getStartOfDay(startDate).getTime(),
    ) / DAY_IN_MS,
  );
  const showYear =
    startDate.getFullYear() !== endDate.getFullYear() || diffInDays > 365;

  return `${formatDate(startDate, showYear)} — ${formatDate(endDate, showYear)}`;
};

const getPresetRange = (
  interval: AnalyticsIntervalKey,
): DateRangeValue | null => {
  const today = getStartOfDay(new Date());

  switch (interval) {
    case "today":
      return [today, today];
    case "yesterday": {
      const yesterday = shiftDays(today, -1);

      return [yesterday, yesterday];
    }
    case "last7":
      return [shiftDays(today, -6), today];
    case "last30":
      return [shiftDays(today, -29), today];
    default:
      return null;
  }
};

const ButtonSelectIntervalDate: React.FC = () => {
  const { modeDesktop } = useContext(AppContext);

  const [actionSheetShown, setActionSheetShown] = useState(false);
  const [selectedInterval, setSelectedInterval] =
    useState<AnalyticsIntervalKey>(INTERVALS[0].key);
  const [previousInterval, setPreviousInterval] =
    useState<AnalyticsIntervalKey>(INTERVALS[0].key);
  const [calendarPlacement, setCalendarPlacement] = useState<any>("bottom-end");
  const [customRange, setCustomRange] = useState<DateRangeValue | null>(null);
  const [desktopCalendarOpen, setDesktopCalendarOpen] = useState(false);
  const [mobileDateModalOpen, setMobileDateModalOpen] = useState(false);
  const targetRef = useRef<HTMLElement>(null);
  const popperRef = useRef<HTMLDivElement>(null);

  const handleCloseActionSheet = () => {
    setActionSheetShown(false);
  };

  const handleCloseCustomPicker = () => {
    if (selectedInterval === "custom" && !hasCompleteRange(customRange)) {
      setSelectedInterval(previousInterval);
      setCustomRange(null);
    }

    setDesktopCalendarOpen(false);
    setMobileDateModalOpen(false);
  };

  const handleOpenActionSheet = () => {
    handleCloseCustomPicker();
    setActionSheetShown(true);
  };

  const handleSelectInterval = (interval: AnalyticsIntervalKey) => {
    if (interval === "custom" && selectedInterval !== "custom") {
      setPreviousInterval(selectedInterval);
    }

    if (interval !== "custom") {
      setCustomRange(null);
    }

    setSelectedInterval(interval);
    handleCloseActionSheet();

    if (interval === "custom") {
      if (modeDesktop) {
        setDesktopCalendarOpen(true);
      } else {
        setMobileDateModalOpen(true);
      }
    } else {
      setDesktopCalendarOpen(false);
      setMobileDateModalOpen(false);
    }
  };

  const handleCustomRangeChange = (value?: DateRangeValue) => {
    if (!value) {
      setCustomRange(null);
      return;
    }

    setCustomRange([value[0], value[1]]);

    if (hasCompleteRange(value)) {
      setDesktopCalendarOpen(false);
    }
  };

  const handleStartDateChange = (value?: Date) => {
    setCustomRange((prevValue) => [value ?? null, prevValue?.[1] ?? null]);
  };

  const handleEndDateChange = (value?: Date) => {
    setCustomRange((prevValue) => [prevValue?.[0] ?? null, value ?? null]);
  };

  const handleApplyCustomRange = () => {
    if (hasCompleteRange(customRange) && !hasInvalidRangeOrder(customRange)) {
      setMobileDateModalOpen(false);
    }
  };

  const shouldDisableStartDate = (value: Date) =>
    Boolean(
      isFutureDay(value) ||
      (customRange?.[1] && isAfterDay(value, customRange[1])),
    );

  const shouldDisableEndDate = (value: Date) =>
    Boolean(
      isFutureDay(value) ||
      (customRange?.[0] && isBeforeDay(value, customRange[0])),
    );

  const buttonLabel = (() => {
    if (selectedInterval === "all") {
      return INTERVALS[0].label;
    }

    if (selectedInterval === "custom") {
      return formatRangeLabel(customRange);
    }

    return formatRangeLabel(
      getPresetRange(selectedInterval),
      INTERVALS.find((interval) => interval.key === selectedInterval)?.label,
    );
  })();

  return (
    <>
      {actionSheetShown ? (
        <AppRootPortal usePortal>
          <ActionSheet toggleRef={targetRef} onClose={handleCloseActionSheet}>
            {INTERVALS.map((interval) => (
              <ActionSheetItem
                key={interval.key}
                selectable
                checked={selectedInterval === interval.key}
                onClick={() => handleSelectInterval(interval.key)}
              >
                {interval.label}
              </ActionSheetItem>
            ))}
          </ActionSheet>
        </AppRootPortal>
      ) : null}
      {!modeDesktop ? (
        <AppRootPortal usePortal>
          <ModalPage
            open={mobileDateModalOpen}
            settlingHeight={100}
            size="m"
            header={
              <ModalPageHeader
                before={
                  <ModalPageHeaderButtonClose
                    onClick={handleCloseCustomPicker}
                  />
                }
              >
                Календарь
              </ModalPageHeader>
            }
            footer={
              <>
                <Separator />
                <Div>
                  <Button
                    appearance="accent"
                    disabled={
                      !hasCompleteRange(customRange) ||
                      hasInvalidRangeOrder(customRange)
                    }
                    mode="primary"
                    size="l"
                    stretched
                    onClick={handleApplyCustomRange}
                  >
                    Применить
                  </Button>
                </Div>
              </>
            }
            onClose={handleCloseCustomPicker}
          >
            <FormItem top="Начало периода">
              <DateInput
                aria-label="Дата начала"
                disableFuture
                placeholder="Дата начала"
                shouldDisableDate={shouldDisableStartDate}
                value={customRange?.[0] ?? undefined}
                onChange={handleStartDateChange}
              />
            </FormItem>
            <FormItem top="Окончание периода">
              <DateInput
                aria-label="Дата окончания"
                disableFuture
                placeholder="Дата окончания"
                shouldDisableDate={shouldDisableEndDate}
                value={customRange?.[1] ?? undefined}
                onChange={handleEndDateChange}
              />
            </FormItem>
          </ModalPage>
        </AppRootPortal>
      ) : null}
      {modeDesktop && desktopCalendarOpen ? (
        <Popper
          getRootRef={popperRef}
          targetRef={targetRef}
          placement={calendarPlacement}
          shown={desktopCalendarOpen}
          onShownChange={(shown) => {
            if (!shown) {
              handleCloseCustomPicker();
            }
          }}
          onPlacementChange={setCalendarPlacement}
          offsetByMainAxis={8}
          usePortal
          zIndex={101}
        >
          <CalendarRange
            role="dialog"
            disableFuture
            value={customRange ?? undefined}
            onChange={handleCustomRangeChange}
          />
        </Popper>
      ) : null}
      <Button
        after={<Icon16DropdownOutline />}
        appearance="neutral"
        getRootRef={targetRef as React.Ref<HTMLElement>}
        mode="tertiary"
        onClick={handleOpenActionSheet}
        size="l"
      >
        {buttonLabel}
      </Button>
    </>
  );
};

export default ButtonSelectIntervalDate;

Карточка видео:

Image
import { Fragment, useContext } from "react";
import { getOptimalImageFromSizes } from "engine/action";
import {
  VKVIDEO_LINK,
  VK_LINK,
  classNames,
  decWord,
  formatTime,
  getDataProfileOrGroup,
  toShort,
} from "engine/utils";

import { AppContext } from "engine/state";

import {
  AspectRatio,
  Card,
  Headline,
  SimpleCell,
  Tappable,
  Link,
} from "@vkontakte/vkui";
import { CustomBadge, CustomAvatar } from "engine/components";

import "./VideoCard.css";

interface VideoCardProps extends React.HTMLAttributes<HTMLDivElement> {
  data: any;
  type: "artists" | "people" | "groups";
}

const VideoCard: React.FC<VideoCardProps> = ({
  data,
  type,
  className,
  ...restProps
}) => {
  const { isDesktop } = useContext(AppContext);
  const image = data?.image || [];
  const url = getOptimalImageFromSizes({ data: image })?.url;

  const isArtists = type === "artists";
  const isPeople = type === "people";
  const isGroup = type === "groups";

  const profileOrGroup = getDataProfileOrGroup({ data, type });

  const id = isGroup
    ? `-${profileOrGroup?.id || 0}`
    : `${profileOrGroup?.id || 0}`;
  const name = isGroup
    ? `${profileOrGroup?.name}`
    : isPeople
    ? `${profileOrGroup?.first_name} ${profileOrGroup?.last_name}`
    : ``;

  const mainLink = isGroup
    ? `${VKVIDEO_LINK}/@club${profileOrGroup?.id || 0}`
    : isPeople
    ? `${VKVIDEO_LINK}/@id${id}`
    : ``;

  const views =
    toShort(data?.views) +
    " " +
    decWord(
      data?.views > 1000 ? 1000 : data?.views,
      ["просмотр", "просмотра", "просмотров"],
      false
    );

  const date = new Date(data?.date * 1000)?.toLocaleDateString(undefined, {
    day: "numeric",
    hour: "2-digit",
    minute: "2-digit",
    month: "short",
    year: "numeric",
  });

  const subtitle = isArtists ? (
    <div className="VideoCard__names">
      {data?.main_artists?.map((value: Record<string, any>, key: number) => (
        <Fragment key={`Link--${key}`}>
          <Link
            className="VideoCard__name"
            href={`${VK_LINK}/artist/${value?.id}`}
            target="_blank"
          >
            {value?.name}
          </Link>
          {data?.main_artists?.length - 1 !== key ? ", " : ""}
        </Fragment>
      ))}
    </div>
  ) : (
    <Link className="VideoCard__name" href={mainLink} target="_blank">
      <div className="VideoCard__name__text">{name}</div>
      <CustomBadge className="VideoCard__name__Badge" data={profileOrGroup} tooltip={false} />
    </Link>
  );

  return (
    <Card
      {...restProps}
      className={classNames(
        "VideoCard",
        isDesktop ? "VideoCard--desktop" : "",
        className
      )}
    >
      <Tappable
        className="VideoCard__content"
        href={data?.video_url}
        target="_blank"
      >
        {data?.live === 1 ? (
          <Headline
            className={classNames(
              "VideoCard__caption",
              "VideoCard__caption--live"
            )}
            level="2"
            weight="2"
          >
            LIVE
          </Headline>
        ) : (
          <Headline className="VideoCard__caption" level="2" weight="2">
            {(data?.platform ? data?.platform + " · " : "") +
              formatTime(data?.duration)}
          </Headline>
        )}
        <AspectRatio className="VideoCard__image" ratio={16 / 9}>
          <img alt="" src={url} />
        </AspectRatio>
      </Tappable>
      <SimpleCell
        className="VideoCard__cell"
        before={<CustomAvatar data={profileOrGroup} size={36} />}
        subtitle={subtitle}
        extraSubtitle={
          <>
            {views}
            <br />
            {date}
          </>
        }
      >
        <Link
          className="VideoCard__title"
          href={data?.video_url}
          target="_blank"
        >
          {data?.title}
        </Link>
      </SimpleCell>
    </Card>
  );
};

export default VideoCard;

import { useContext } from "react";
import { VK_LINK } from "engine/utils";

import { AppContext } from "engine/state";

import { Tooltip, Link } from "@vkontakte/vkui";

import { Icon12Verified, Icon12Crown } from "@vkontakte/icons";

import "./CustomBadge.css";

interface CustomBadgeProps extends React.HTMLAttributes<SVGSVGElement> {
  data: {
    [key: string]: any;
    is_verified: boolean;
    verified: number;
  };
  tooltip?: boolean;
}

const CustomBadge: React.FC<CustomBadgeProps> = ({
  data,
  tooltip = true,
  className,
  ...restProps
}) => {
  const { isDesktop } = useContext(AppContext);
  const confirmed = data?.is_verified;
  const verified = data?.verified === 1;
  const isGoldenMarkedBusiness = data?.is_golden_marked_business;
  const isConfirmedBusiness = data?.is_confirmed_business;
  const iconVerifiedColor = verified
    ? "var(--vkui--color_icon_accent)"
    : "var(--vkui--color_icon_secondary)";
  return (
    <div className="CustomBadge">
      {confirmed || verified ? (
        <Tooltip
          enableInteractive={true}
          title={
            verified
              ? "Верифицированный профиль"
              : confirmed
              ? "Подтверждённый аккаунт"
              : ""
          }
          maxWidth={null}
          placement="top"
          shown={tooltip ? (isDesktop ? undefined : false) : false}
          description={
            verified ? (
              <span>
                Профиль верифицирован <br />
                <Link target="_blank" href={`${VK_LINK}/blog/verification`}>
                  Узнайте больше о новой верификации
                </Link>
              </span>
            ) : confirmed ? (
              <span>
                Данные аккаунта подтверждены <br />
                <Link target="_blank" href={`${VK_LINK}/faq20572`}>
                  Подробнее
                </Link>
              </span>
            ) : (
              ""
            )
          }
        >
          <Icon12Verified
            color={iconVerifiedColor}
            style={{ minWidth: 16, minHeight: 16, cursor: "pointer" }}
          />
        </Tooltip>
      ) : isGoldenMarkedBusiness ? (
        <Tooltip
          enableInteractive={true}
          maxWidth={null}
          placement="top"
          shown={tooltip ? (isDesktop ? undefined : false) : false}
          description={
            <span>
              Компания подтвердила свою надёжность
              <br /> и пользуется сервисами VK для бизнеса
              <br />
              <Link
                target="_blank"
                href="https://vk.ru/@business-otmetky-dlya-biznesa?anchor=otmetka-premium-biznes"
              >
                Подробнее
              </Link>
            </span>
          }
        >
          <Icon12Crown
            color="var(--vkui--color_icon_accent)"
            style={{ minWidth: 16, minHeight: 16, cursor: "pointer" }}
          />
        </Tooltip>
      ) : isConfirmedBusiness ? (
        <Tooltip
          enableInteractive={true}
          maxWidth={null}
          placement="top"
          shown={tooltip ? (isDesktop ? undefined : false) : false}
          description={
            <span>
              Данные компании подтверждены
              <br /> через сервис VK Бизнес ID
              <br />
              <Link
                target="_blank"
                href="https://vk.ru/@business-otmetky-dlya-biznesa?anchor=otmetka-podtverzhd-nny-biznes"
              >
                Подробнее
              </Link>
            </span>
          }
        >
          <Icon12Verified
            color="var(--vkui--color_icon_secondary)"
            style={{ minWidth: 16, minHeight: 16, cursor: "pointer" }}
          />
        </Tooltip>
      ) : (
        <></>
      )}
    </div>
  );
};

export default CustomBadge;

import { useContext } from "react";
import { bridge } from "engine/action";
import { VK_LINK, classNames } from "engine/utils";

import { AppContext } from "engine/state";

import { Avatar, Tappable } from "@vkontakte/vkui";

import {
  Icon16BloggerMark10kOutline,
  Icon28MagnifierPlus,
} from "@vkontakte/icons";

// import "./CustomAvatar.css";

interface CustomAvatarProps extends React.HTMLAttributes<HTMLDivElement> {
  data: {
    [key: string]: any;
    icon_150: string;
    online: number;
    online_app: boolean;
    online_mobile: number;
    photo_100: string;
    photo_max_orig: string;
  };
  size?: number;
}

const CustomAvatar: React.FC<CustomAvatarProps> = ({
  data,
  size,
  className,
  ...restProps
}) => {
  const { isDesktop } = useContext(AppContext);
  const photo =
    data?.photo_100 || data?.icon_150 || `${VK_LINK}/images/camera_100.png`;
  const photoMaxOrig = data?.photo_max_orig;
  const photoShowed = isDesktop && photoMaxOrig && !size;
  const preset =
    data?.online_mobile === 1 || data?.online_app
      ? "online-mobile"
      : data?.online === 1
      ? "online"
      : undefined;
  const avatarSize = size ? size : isDesktop ? 80 : 48;
  return (
    <Tappable
      activeMode="none"
      hoverMode="none"
      onClick={
        photoShowed
          ? () => bridge.send("VKWebAppShowImages", { images: [photoMaxOrig] })
          : undefined
      }
    >
      <Avatar
        {...restProps}
        className={classNames("CustomAvatar", className)}
        size={avatarSize}
        src={photo}
      >
        {data?.a_plus_mark ? (
          <Avatar.Badge
            style={{
              display: "flex",
              flexDirection: "column",
              justifyContent: "center",
              height: 20,
              borderRadius: 4,
              padding: "0 6px",
              backgroundColor: "var(--vkui--color_overlay_primary)",
            }}
          >
            <Icon16BloggerMark10kOutline
              color="#fff"
              height={12}
              width={12}
              style={{
                margin: 2,
              }}
            />
          </Avatar.Badge>
        ) : preset ? (
          <Avatar.BadgeWithPreset preset={preset} />
        ) : (
          <></>
        )}
        {photoShowed && !size ? (
          <Avatar.Overlay
            aria-label="Кнопка для просмотра изображения"
            theme="dark"
          >
            <Icon28MagnifierPlus />
          </Avatar.Overlay>
        ) : (
          <></>
        )}
      </Avatar>
    </Tappable>
  );
};

export default CustomAvatar;

Карточка сообщества и пользователя:

Image Image
import { useMemo, useContext } from "react";
import {
  VK_LINK_CLUB,
  VK_LINK_ID,
  calculateAge,
  classNames,
  decWord,
  toShort,
} from "engine/utils";

import { useGlobalValue } from "elum-state/react";
import { AppContext, USER_INFO } from "engine/state";

import {
  AspectRatio,
  Caption,
  Card,
  Footnote,
  Headline,
  Tappable,
  UsersStack,
} from "@vkontakte/vkui";
import { CustomBadge } from "engine/components";

import { Icon16BloggerMark10kOutline } from "@vkontakte/icons";

import { TKeySearchParams } from "engine/types";

import "./UserCard.css";

interface UserCardProps extends React.HTMLAttributes<HTMLDivElement> {
  data: any;
  type: TKeySearchParams;
}

const UserCard: React.FC<UserCardProps> = ({
  data,
  type,
  className,
  ...restProps
}) => {
  const { isDesktop } = useContext(AppContext);
  const user = useGlobalValue(USER_INFO);

  const isPeople = type === "people";
  const isGroup = type === "groups";
  const isCurrentUser = user?.id === data?.id;
  const commonFriendsCount = data?.common_friends_count || 0;
  const commonFriendsPhotos = data?.common_friends_photos || [];
  const hasMutualFriends = commonFriendsCount > 0;
  const commonFriendsText = `${decWord(commonFriendsCount, [
    "общий",
    "общих",
    "общих",
  ])}`;
  const isFriend = data?.is_friend;
  const isFriendText = "У вас в друзьях";

  const id = isGroup ? `-${data?.id || 0}` : `${data?.id || 0}`;
  const name = isGroup
    ? `${data?.name}`
    : isPeople
    ? `${data?.first_name} ${data?.last_name}`
    : ``;

  const mainLink = isGroup
    ? `${VK_LINK_CLUB}${data?.id || 0}`
    : isPeople
    ? `${VK_LINK_ID}${id}`
    : ``;

  const url = data?.photo_max_orig;

  const age = useMemo(
    () =>
      data?.bdate?.split(".")?.[0] &&
      data?.bdate?.split(".")?.[1] &&
      data?.bdate?.split(".")?.[2]
        ? calculateAge(
            data?.bdate?.split(".")?.[0],
            data?.bdate?.split(".")?.[1],
            data?.bdate?.split(".")?.[2]
          )
        : undefined,
    [data?.bdate]
  );

  const city = data?.city?.title;
  const company = data?.career?.reverse()?.[0]?.company;

  const subtitle = isPeople
    ? isCurrentUser
      ? "Это вы. И это прекрасно."
      : (age ? decWord(age, ["год", "года", "лет"]) : "") +
        (company ? (age ? ", " : "") + company : "") +
        (city ? (age || company ? ", " : "") + city : "")
    : isGroup
    ? data?.activity
    : "";

  return (
    <Card
      {...restProps}
      className={classNames(
        "UserCard",
        isDesktop ? "UserCard--desktop" : "",
        className
      )}
    >
      <Tappable className="UserCard__content" href={mainLink} target="_blank">
        <AspectRatio className="UserCard__image" ratio={1 / 1}>
          <img alt="" src={url} />
          <div className="UserCard__image__in">
            {(isGroup && data?.members_count) ||
            (isPeople && (isFriend || hasMutualFriends)) ? (
              <div
                className={classNames("UserCard__badge", {
                  "UserCard__badge--with_UsersStack": hasMutualFriends,
                })}
              >
                <Caption className="EllipsisText">
                  {isGroup ? (
                    data?.members_count ? (
                      toShort(
                        data?.members_count,
                        ["", "K", "M", "G", "T", "P"],
                        false
                      ) +
                      " " +
                      decWord(
                        data?.members_count,
                        ["подписчик", "подписчика", "подписчиков"],
                        false
                      )
                    ) : (
                      ""
                    )
                  ) : isFriend ? (
                    isFriendText
                  ) : hasMutualFriends ? (
                    <UsersStack
                      className="UserCard__UsersStack"
                      photos={commonFriendsPhotos}
                      size="s"
                    >
                      {commonFriendsText}
                    </UsersStack>
                  ) : (
                    ""
                  )}
                </Caption>
              </div>
            ) : (
              <></>
            )}
            {data?.a_plus_mark ? (
              <div className="UserCard__badge-top">
                <Icon16BloggerMark10kOutline
                  color="#fff"
                  height={12}
                  width={12}
                  style={{
                    margin: 2,
                  }}
                />
              </div>
            ) : (
              <></>
            )}
          </div>
        </AspectRatio>
        <div className="UserCard__caption">
          <div className="UserCard__title">
            <Headline className="UserCard__headline EllipsisText" level="1">
              {name}
            </Headline>
            <CustomBadge className="UserCard__verify" data={data} />
          </div>
          <Footnote className="UserCard__subtitle EllipsisText">
            {subtitle}
          </Footnote>
        </div>
      </Tappable>
    </Card>
  );
};

export default UserCard;

и тд.

Metadata

Metadata

Assignees

Labels

Type

Projects

Status

🗃 Backlog

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions