Пример UX на обзорной странице

Как я разработал обзорную страницу для всех категорий постов

Цели UI и UX

Моя главная цель с обзорной страницей, на которой показаны все категории опубликованных сообщений, заключалась в том, чтобы повысить обнаруживаемость категорий, которые фактически доступны. Я также рассматривал это изменение как возможность улучшить визуальную красоту страницы, а также согласовать UX с другими страницами в этом прогрессивном веб-приложении.

На следующем снимке экрана показано, как страница выглядела до редизайна.

Image e8edca7d7ea0

Что нужно было изменить

В старой реализации были две основные проблемы. Во-первых, макет был фактически недействительным, поскольку категории с менее чем 5 элементами масштабировали доступные изображения обложек после предварительного просмотра, чтобы они соответствовали всей ширине. Во-вторых, не было основной панели действий, видимой в верхней части страницы - она обязательно должна быть отображена, поскольку она предоставляет пользователю основные элементы навигации.

Более того, становится очевидным, что страница обзора категорий на самом деле не очень хорошо справлялась с их отображением. Вместо этого пользователь в основном видел изображения обложек для сообщений, относящихся к каждой категории. Только при втором взгляде можно распознать строку со значком, а также заголовок каждой категории. Помимо того, что этот пользовательский интерфейс не эстетичен, он просто был плохо спроектирован.

Исправление ошибок

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

В новой версии отображается один столбец строк, где каждый элемент показывает большое изображение предварительного просмотра для категории. Пользовательское изображение занимает всю разрешенную ширину. По центру обеих осей находится заголовок категории посередине, что делает его сразу видимым и узнаваемым. Чтобы улучшить читаемость, я добавил тонкий градиент от черного к прозрачному под текстом.

При наведении курсора на изображение и текст становится видимым дополнительный информационный текст, показывающий общее количество сообщений для этой категории - информация, которая ранее не была доступна.

Чтобы решить проблему с навигацией, я также повторно использовал существующую панель действий со всеми основными маршрутами, доступными пользователю.

Image 0616bdd11b98

Улучшение через итерацию

Чтобы обеспечить более приятное взаимодействие с пользователем, я также добавил к изображению тонкую анимацию масштабирования при наведении курсора на строку категории. Поскольку изображение масштабируется в исходных границах, вид не увеличивается без надобности.

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

Чтобы создать иллюзию большего количества контента, чем есть на самом деле, я использовал класс «stack» Daisy-UI, чтобы придать каждому пост-превью фальшивый стек, чтобы создать впечатление о большем количестве элементов предварительного просмотра под текущими.

Image 41c052a8c928

На небольших устройствах предварительный просмотр не выполняется. Пользователь может только щелкнуть большое изображение категории, чтобы перейти на страницу с подробностями, где перечислены все сообщения.

Обзор и прогноз

Как видите, даже простая страница, на которой должен быть только обзор элементов, может быть источником как визуальных, так и концептуальных ошибок, а также учебного материала о том, как разработать хороший и работающий пользовательский интерфейс, а также UX.

Я не добавлял локальный текстовый поиск, чтобы пользователи могли фильтровать его по вводу текста. На данный момент я намерен продвигать исследование страницы с помощью прокрутки или наведения курсора. Если это станет проблемой в будущем (например, из-за того, что пользователи не могут найти контент, который они ищут), я добавлю несколько параметров фильтрации. Это также может стать необходимым, поскольку количество постов и категорий продолжает расти. Конечно, я приветствую вас, чтобы вы сами опробовали эту страницу.

Исходный код

Следующий текст содержит полный неизмененный код как для контейнера страницы, так и для групп категорий. Все написано на Typescript с React.js и Tailwind.css. Если вы думаете, что можете повторно использовать некоторые детали, я рад, что смог вам помочь.

//
// Page container.
//

import React from "react";
import { CMSCategoryResolution } from "../../cms/entities/cms.entity.catgory";
import Reveal from "../Reveal/Reveal";
import { BogPostCategoryPostsGroup } from "./BlogPostCategoryPostsGroup";
import BlogPostCategoriesOverviewHero from "./BlogPostCategoriesOverviewHero";
import PrimaryActionBar from "../PrimaryActionsBar/PrimaryActionsBar";

/*
 *
 * Interfaces.
 *
 */

interface Props {
  categories: CMSCategoryResolution[];
}

/*
 *
 * Components.
 *
 */

export default function BlogPostCategoriesOverview(props: Props) {
  const { categories } = props;

  return (
    <div className="max-w-6xl mx-auto">
      <BlogPostCategoriesOverviewHero />
      <div className="mt-10 mb-20">
        <PrimaryActionBar isCentered color="white" />
      </div>
      <div className="px-6 mx-auto space-y-6 lg:px-0 md:space-y-16 lg:space-y-20">
        {categories.map((c, i) => (
          <Reveal key={i}>
            <BogPostCategoryPostsGroup category={c} maxPostsEachThreshold={20} />
          </Reveal>
        ))}
      </div>
    </div>
  );
}

//
// BogPostCategoryPostsGroup.
//

import React from "react";
import { CMSCategoryResolution } from "../../cms/entities/cms.entity.catgory";
import { FLPath } from "../../models/route/model.route";
import { CMSCategoryIconFactory } from "../CMS/CMSCategoryIcon";
import Link from "next/link";
import RiMore from "remixicon-react/MoreLineIcon";

/*
 *
 * Interfaces.
 *
 */

interface Props {
  category: CMSCategoryResolution;
  maxPostsEachThreshold: number;
}

/*
 *
 * Components.
 *
 */

export const BogPostCategoryPostsGroup = (props: Props) => {
  const { category, maxPostsEachThreshold } = props;
  const { posts } = category;

  if (posts.length === 0) {
    return null;
  }

  const url = `${FLPath.Categories}/${category.slug.current}`;
  const Icon = CMSCategoryIconFactory({ variant: category.slug.current });

  return (
    <div className="flex flex-col items-center group">
      <div className="relative max-w-4xl overflow-visible rounded-md">
        <div className="absolute items-end justify-start hidden w-full h-full px-6 space-x-4 overflow-x-scroll duration-500 ease-in-out translate-y-12 opacity-0 md:flex pb-14 group-hover:opacity-100 group-hover:translate-y-20">
          {posts.slice(0, 4).map((p, i) => (
            <span className="stack">
              <Link
                href={`${FLPath.Posts}/${p.slug.current}`}
                aria-label={p.title}
                prefetch={false}>
                <a className="duration-200 ease-out hover:scale-105">
                  <img
                    src={p.imageUrl.jpg}
                    alt={p.title}
                    loading="lazy"
                    className="object-contain h-20 rounded "
                  />
                </a>
              </Link>

              {Array.from({ length: 2 }, (_, i) => (
                <span key={i} style={{ width: 152, height: 80 }} className="bg-gray-800 rounded" />
              ))}
            </span>
          ))}
          {posts.length >= 5 && (
            <Link passHref href={url} aria-label={`Link to ${category.title}`} prefetch={false}>
              <a className="flex flex-col items-center justify-center h-20 w-14">
                <RiMore />
              </a>
            </Link>
          )}
        </div>

        <div className="relative flex flex-col items-center overflow-hidden duration-500 ease-in-out rounded-md shadow-xl md:group-hover:-translate-y-20">
          <img
            className="object-cover w-full duration-500 ease-in-out group-hover:scale-105"
            width={960}
            height={200}
            src={category.imageUrl.webp}
            alt={category.title}
            loading="lazy"
          />
          <Link passHref href={url} aria-label={`Link to ${category.title}`} prefetch={false}>
            <a className="absolute flex flex-col items-center justify-center w-full h-full bg-gradient-to-t from-primary">
              <span className="flex items-center space-x-2 duration-300 ease-in-out translate-y-2 group-hover:-translate-y-3">
                <Icon size="2em" />
                <h2 className="text-sm md:text-xl">{category.title}</h2>
              </span>
              <span className="text-sm font-light text-gray-200 transition duration-500 ease-in-out translate-y-2 opacity-0 group-hover:opacity-100 group-hover:-translate-y-2">
                {category.posts.length}
                {category.posts.length === maxPostsEachThreshold ? "+" : ""}{" "}
                {category.posts.length === 1 ? "post" : "posts"}
              </span>
            </a>
          </Link>
        </div>
      </div>
    </div>
  );
};