دراسة حالة UX لصفحة نظرة عامة

كيف صممت صفحة النظرة العامة لجميع فئات التدوينات

أهداف UI و UX

كان هدفي الرئيسي من خلال صفحة نظرة عامة تعرض جميع فئات المشاركات المنشورة هو زيادة قابلية اكتشاف الفئات المتوفرة بالفعل. لقد رأيت أيضًا هذا التغيير كفرصة لتحسين الجمال المرئي للصفحة بالإضافة إلى محاذاة UX مع الصفحات الأخرى في تطبيق الويب التدريجي هذا.

توضح لقطة الشاشة التالية كيف بدت الصفحة قبل إعادة التصميم.

Image e8edca7d7ea0

ما الذي يجب أن يتغير

كان التنفيذ القديم قضيتين رئيسيتين. أولاً ، كان التخطيط غير صالح في الواقع ، حيث قامت الفئات التي تحتوي على أقل من 5 عناصر بقياس صور غلاف ما بعد المعاينة المتاحة لتناسب العرض بالكامل. ثانيًا ، لم يكن هناك شريط إجراءات أساسي مرئي في الجزء العلوي من الصفحة - يجب بالتأكيد أن يكون هناك شريط واحد معروض ، لأنه يوفر عناصر التنقل الرئيسية للمستخدم.

علاوة على ذلك ، يتضح أن صفحة النظرة العامة للفئات لم تقم بعمل رائع في الواقع لجعلها مرئية. بدلاً من ذلك ، رأى المستخدم في المقام الأول صور غلاف للمشاركات المرتبطة بكل فئة. فقط عند إلقاء نظرة ثانية ، يتم التعرف على الصف الذي يحتوي على رمز بالإضافة إلى عنوان كل فئة. بصرف النظر عن كونها غير مرضية من الناحية الجمالية ، فقد تم تصميم واجهة المستخدم هذه بشكل سيئ.

تصحيح الاخطاء

نظرًا لأن هذا هو تطبيق الويب الشخصي الخاص بي ، فقد كنت أيضًا الشخص الذي أفسد الأمور في البداية في صفحة نظرة عامة على الفئات. دون مزيد من الأعذار ، جلست لإعادة صياغة الصفحة بأكملها.

يعرض الإصدار الجديد عمودًا واحدًا من الصفوف ، حيث يعرض كل عنصر صورة معاينة كبيرة لفئة. تأخذ الصورة المخصصة العرض الكامل المسموح به. يتوسط كلا المحورين عنوان الفئة في المنتصف ، مما يجعله مرئيًا ويمكن التعرف عليه على الفور. لتحسين الوضوح ، أضفت تدرجًا دقيقًا من الأسود إلى الشفاف أسفل النص.

عند التمرير فوق الصورة والنص ، يصبح نص معلومات إضافي مرئيًا ، يعرض العدد الإجمالي للمشاركات لهذه الفئة - وهي معلومات لم تكن متاحة من قبل.

لإصلاح مشكلة التنقل ، قمت أيضًا بإعادة استخدام شريط الإجراءات الحالي مع جميع المسارات الرئيسية المتاحة للمستخدم.

Image 0616bdd11b98

التحسين من خلال التكرار

لتوفير تجربة مستخدم أكثر متعة ، أضفت أيضًا رسم متحرك دقيق للصورة عند التمرير فوق صف فئة. مع تحجيم الصورة داخل حدودها الأصلية ، لا ينمو العرض بشكل غير ضروري.

علاوة على ذلك ، يصبح صف جديد أسفل صورة الفئة مرئيًا من خلال الرسوم المتحركة عند التمرير فوقه. يعرض هذا الصف مجموعة مختارة من أحدث المشاركات للفئة. كل معاينة لاحقة هي رابط إلى المنشور. يفتح كل عنصر تفاعلي آخر في صف الفئة صفحة التفاصيل للفئة.

لإعطاء انطباع بوجود محتوى أكثر مما هو موجود بالفعل ، استخدمت فئة Daisy-UIs "المكدس" لإعطاء كل معاينة لاحقة مكدسًا مزيفًا للإشارة إلى انطباع عن المزيد من عناصر المعاينة أسفل العناصر الحالية.

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>
  );
};