לדלג לתוכן

8.1 סקירת גישות לעיצוב פתרון

פתרון - סקירת גישות לעיצוב

פתרון תרגיל 1

interface StatusBadgeProps {
  status: "success" | "warning" | "error";
  children: React.ReactNode;
}

function StatusBadge({ status, children }: StatusBadgeProps) {
  const colorMap: Record<string, string> = {
    success: "#4CAF50",
    warning: "#FF9800",
    error: "#F44336",
  };

  const style: React.CSSProperties = {
    backgroundColor: colorMap[status],
    color: "white",
    padding: "4px 12px",
    borderRadius: "12px",
    fontSize: "14px",
    fontWeight: 500,
    display: "inline-block",
  };

  return <span style={style}>{children}</span>;
}

// שימוש
function App() {
  return (
    <div style={{ display: "flex", gap: "8px" }}>
      <StatusBadge status="success">הצלחה</StatusBadge>
      <StatusBadge status="warning">אזהרה</StatusBadge>
      <StatusBadge status="error">שגיאה</StatusBadge>
    </div>
  );
}
  • השתמשנו ב-Record למיפוי צבעים לפי סטטוס
  • הטיפוס React.CSSProperties מבטיח שכל התכונות תקינות
  • הגישה של inline styles מתאימה כאן כי הסגנון דינמי ופשוט

פתרון תרגיל 2

/* ProfileCard.css */
.profile-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 24px;
  border: 1px solid #e0e0e0;
  border-radius: 12px;
  max-width: 300px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.profile-card__avatar {
  width: 80px;
  height: 80px;
  border-radius: 50%;
  object-fit: cover;
  margin-bottom: 12px;
}

.profile-card__name {
  font-size: 20px;
  font-weight: bold;
  margin: 0 0 4px 0;
}

.profile-card__bio {
  font-size: 14px;
  color: #666;
  text-align: center;
  margin: 0 0 16px 0;
}

.profile-card__button {
  padding: 8px 24px;
  border: 2px solid #007bff;
  border-radius: 20px;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.2s ease;
}

.profile-card__button--follow {
  background-color: #007bff;
  color: white;
}

.profile-card__button--follow:hover {
  background-color: #0056b3;
}

.profile-card__button--following {
  background-color: white;
  color: #007bff;
}

.profile-card__button--following:hover {
  background-color: #f0f0f0;
}
// ProfileCard.tsx
import clsx from "clsx";
import "./ProfileCard.css";

interface ProfileCardProps {
  name: string;
  bio: string;
  avatarUrl: string;
  isFollowing: boolean;
  onToggleFollow: () => void;
}

function ProfileCard({
  name,
  bio,
  avatarUrl,
  isFollowing,
  onToggleFollow,
}: ProfileCardProps) {
  return (
    <div className="profile-card">
      <img className="profile-card__avatar" src={avatarUrl} alt={name} />
      <h3 className="profile-card__name">{name}</h3>
      <p className="profile-card__bio">{bio}</p>
      <button
        className={clsx("profile-card__button", {
          "profile-card__button--follow": !isFollowing,
          "profile-card__button--following": isFollowing,
        })}
        onClick={onToggleFollow}
      >
        {isFollowing ? "עוקב" : "עקוב"}
      </button>
    </div>
  );
}
  • השתמשנו בשיטת BEM למניעת התנגשויות שמות
  • clsx מאפשר לנו להחליף בין מחלקות בצורה נקייה
  • שימו לב לשימוש ב-transition עבור אנימציית hover חלקה

פתרון תרגיל 3

/* ProfileCard.module.css */
.card {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 24px;
  border: 1px solid #e0e0e0;
  border-radius: 12px;
  max-width: 300px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.avatar {
  width: 80px;
  height: 80px;
  border-radius: 50%;
  object-fit: cover;
  margin-bottom: 12px;
}

.name {
  font-size: 20px;
  font-weight: bold;
  margin: 0 0 4px 0;
}

.bio {
  font-size: 14px;
  color: #666;
  text-align: center;
  margin: 0 0 16px 0;
}

.button {
  padding: 8px 24px;
  border: 2px solid #007bff;
  border-radius: 20px;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.2s ease;
}

.followButton {
  composes: button;
  background-color: #007bff;
  color: white;
}

.followButton:hover {
  background-color: #0056b3;
}

.followingButton {
  composes: button;
  background-color: white;
  color: #007bff;
}

.followingButton:hover {
  background-color: #f0f0f0;
}
// ProfileCard.tsx
import styles from "./ProfileCard.module.css";

interface ProfileCardProps {
  name: string;
  bio: string;
  avatarUrl: string;
  isFollowing: boolean;
  onToggleFollow: () => void;
}

function ProfileCard({
  name,
  bio,
  avatarUrl,
  isFollowing,
  onToggleFollow,
}: ProfileCardProps) {
  return (
    <div className={styles.card}>
      <img className={styles.avatar} src={avatarUrl} alt={name} />
      <h3 className={styles.name}>{name}</h3>
      <p className={styles.bio}>{bio}</p>
      <button
        className={isFollowing ? styles.followingButton : styles.followButton}
        onClick={onToggleFollow}
      >
        {isFollowing ? "עוקב" : "עקוב"}
      </button>
    </div>
  );
}
  • עם CSS Modules אין צורך בשמות BEM ארוכים - השמות יהיו ייחודיים אוטומטית
  • composes מאפשר לנו לשתף סגנונות בין מחלקות
  • הקוד נקי יותר כי אין צורך ב-clsx לרוב המקרים

פתרון תרגיל 4

import styled, { css } from "styled-components";

type Variant = "filled" | "outline" | "ghost";
type Size = "sm" | "md" | "lg";

const sizeStyles = {
  sm: css`
    padding: 6px 12px;
    font-size: 12px;
  `,
  md: css`
    padding: 10px 20px;
    font-size: 14px;
  `,
  lg: css`
    padding: 14px 28px;
    font-size: 16px;
  `,
};

const variantStyles = {
  filled: css`
    background-color: #007bff;
    color: white;
    border: none;

    &:hover {
      background-color: #0056b3;
      transform: translateY(-1px);
    }
  `,
  outline: css`
    background-color: transparent;
    color: #007bff;
    border: 2px solid #007bff;

    &:hover {
      background-color: #007bff;
      color: white;
    }
  `,
  ghost: css`
    background-color: transparent;
    color: #007bff;
    border: none;

    &:hover {
      background-color: rgba(0, 123, 255, 0.1);
    }
  `,
};

const StyledThemeButton = styled.button<{ $variant: Variant; $size: Size }>`
  border-radius: 6px;
  cursor: pointer;
  font-weight: 500;
  transition: all 0.2s ease;

  ${(props) => sizeStyles[props.$size]}
  ${(props) => variantStyles[props.$variant]}

  &:active {
    transform: scale(0.98);
  }
`;

interface ThemeButtonProps {
  variant?: Variant;
  size?: Size;
  children: React.ReactNode;
  onClick?: () => void;
}

function ThemeButton({
  variant = "filled",
  size = "md",
  children,
  onClick,
}: ThemeButtonProps) {
  return (
    <StyledThemeButton $variant={variant} $size={size} onClick={onClick}>
      {children}
    </StyledThemeButton>
  );
}

// שימוש
function App() {
  return (
    <div style={{ display: "flex", gap: "12px", padding: "20px" }}>
      <ThemeButton variant="filled" size="sm">קטן</ThemeButton>
      <ThemeButton variant="outline" size="md">בינוני</ThemeButton>
      <ThemeButton variant="ghost" size="lg">גדול</ThemeButton>
    </div>
  );
}
  • הפרדנו את הסגנונות לאובייקטים נפרדים לפי variant ו-size
  • השתמשנו ב-css helper של styled-components עבור קטעי סגנון
  • $ prefix ב-props מונע העברה ל-DOM

פתרון תרגיל 5

interface FeatureCardProps {
  icon: string;
  title: string;
  description: string;
}

function FeatureCard({ icon, title, description }: FeatureCardProps) {
  return (
    <div className="w-full md:w-80 p-6 bg-white border border-gray-200 rounded-xl shadow-sm hover:shadow-lg transition-shadow duration-300 cursor-pointer">
      <div className="text-4xl mb-4">{icon}</div>
      <h3 className="text-lg md:text-xl font-bold text-gray-800 mb-2">
        {title}
      </h3>
      <p className="text-sm md:text-base text-gray-600 leading-relaxed">
        {description}
      </p>
    </div>
  );
}

// שימוש
function FeaturesSection() {
  const features = [
    { icon: "#", title: "מהיר", description: "ביצועים גבוהים ללא פשרות" },
    { icon: "*", title: "מאובטח", description: "אבטחה מובנית בכל שכבה" },
    { icon: "+", title: "גמיש", description: "מתאים לכל פרויקט ודרישה" },
  ];

  return (
    <div className="flex flex-col md:flex-row gap-6 p-8 justify-center">
      {features.map((feature) => (
        <FeatureCard key={feature.title} {...feature} />
      ))}
    </div>
  );
}
  • w-full בטלפון, md:w-80 בדסקטופ
  • hover:shadow-lg ו-transition-shadow ליצירת אפקט הרמה
  • שימוש ב-responsive prefixes (md:) לעיצוב רספונסיבי

פתרון תרגיל 6

import { Card as MantineCard, Text, Button, Group } from "@mantine/core";
import styles from "./ComparisonCard.module.css";

// גישה 1 - Inline Styles
function InlineCard() {
  return (
    <div
      style={{
        padding: "16px",
        border: "1px solid #e0e0e0",
        borderRadius: "8px",
        maxWidth: "250px",
      }}
    >
      <h3 style={{ margin: "0 0 8px 0", fontSize: "18px" }}>כותרת</h3>
      <p style={{ margin: "0 0 12px 0", color: "#666", fontSize: "14px" }}>
        תוכן הכרטיסייה עם תיאור קצר
      </p>
      <button
        style={{
          padding: "8px 16px",
          backgroundColor: "#007bff",
          color: "white",
          border: "none",
          borderRadius: "4px",
          cursor: "pointer",
        }}
      >
        לחץ כאן
      </button>
    </div>
  );
}

// גישה 2 - CSS Modules
function ModulesCard() {
  return (
    <div className={styles.card}>
      <h3 className={styles.title}>כותרת</h3>
      <p className={styles.description}>תוכן הכרטיסייה עם תיאור קצר</p>
      <button className={styles.button}>לחץ כאן</button>
    </div>
  );
}

// גישה 3 - Tailwind
function TailwindCard() {
  return (
    <div className="p-4 border border-gray-200 rounded-lg max-w-[250px]">
      <h3 className="text-lg font-bold mb-2">כותרת</h3>
      <p className="text-sm text-gray-500 mb-3">
        תוכן הכרטיסייה עם תיאור קצר
      </p>
      <button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">
        לחץ כאן
      </button>
    </div>
  );
}

// גישה 4 - Mantine
function MantineCardExample() {
  return (
    <MantineCard shadow="sm" padding="md" radius="md" withBorder maw={250}>
      <Text fw={700} size="lg" mb={8}>
        כותרת
      </Text>
      <Text size="sm" c="dimmed" mb={12}>
        תוכן הכרטיסייה עם תיאור קצר
      </Text>
      <Button variant="filled" size="sm">
        לחץ כאן
      </Button>
    </MantineCard>
  );
}

// דף השוואה
function ComparisonPage() {
  const cards = [
    { title: "Inline Styles", component: <InlineCard /> },
    { title: "CSS Modules", component: <ModulesCard /> },
    { title: "Tailwind CSS", component: <TailwindCard /> },
    { title: "Mantine", component: <MantineCardExample /> },
  ];

  return (
    <div style={{ padding: "40px" }}>
      <h1 style={{ textAlign: "center", marginBottom: "32px" }}>
        השוואת גישות עיצוב
      </h1>
      <div
        style={{
          display: "grid",
          gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))",
          gap: "24px",
        }}
      >
        {cards.map((card) => (
          <div key={card.title}>
            <h2
              style={{
                textAlign: "center",
                marginBottom: "12px",
                color: "#333",
              }}
            >
              {card.title}
            </h2>
            <div style={{ display: "flex", justifyContent: "center" }}>
              {card.component}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}
/* ComparisonCard.module.css */
.card {
  padding: 16px;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  max-width: 250px;
}

.title {
  margin: 0 0 8px 0;
  font-size: 18px;
}

.description {
  margin: 0 0 12px 0;
  color: #666;
  font-size: 14px;
}

.button {
  padding: 8px 16px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: background-color 0.2s;
}

.button:hover {
  background-color: #0056b3;
}
  • כל ארבע הגישות מייצרות כרטיס דומה חזותית
  • הבדלים בכמות הקוד ובגישה - Mantine הכי קצר, inline הכי ארוך

תשובות לשאלות

  1. CSS רגיל לעומת CSS Modules מבחינת scope - ב-CSS רגיל כל המחלקות הן גלובליות ויכולות להתנגש עם מחלקות באותו שם בקומפוננטות אחרות. ב-CSS Modules השמות עוברים hash אוטומטי (למשל .card הופך ל-.Card_card_x7g3k) כך שכל מחלקה ייחודית ומקומית לקומפוננטה.

  2. למה styled-components עלולה לפגוע בביצועים - styled-components מייצרת CSS בזמן ריצה (runtime). בכל רנדור, הספריה צריכה לחשב את הסגנונות, ליצור מחרוזות CSS, ולהזריק אותן ל-DOM דרך תגיות style. CSS Modules, לעומת זאת, מייצרות קבצי CSS סטטיים בזמן build, והדפדפן מטפל בהם ישירות ללא עלות JavaScript.

  3. יתרון וחיסרון של Tailwind - היתרון: מהירות פיתוח גבוהה מאוד, אין צורך לחשוב על שמות מחלקות, והקובץ הסופי קטן כי Tailwind מסיר מחלקות שלא בשימוש. החיסרון: ה-HTML הופך ארוך ומלא מחלקות, מה שפוגע בקריאות, וקשה יותר לעשות שינויים עיצוביים גלובליים.

  4. גישה לדשבורד אדמין - ספרית קומפוננטות כמו Mantine היא הבחירה הטובה ביותר. דשבורד דורש טבלאות, טפסים, modals, ניווט וגרפים - כולם קומפוננטות מורכבות שלא כדאי לבנות מאפס. Mantine מספקת את כולם מוכנים עם נגישות מובנית, וה-theme המאוחד מבטיח עקביות בעיצוב.

  5. מתי inline styles מתאימים ומתי לא - מתאימים: סגנונות דינמיים פשוטים שמשתנים על בסיס props או state (כמו רוחב של progress bar, מיקום של tooltip). לא מתאימים: כשצריך hover effects, media queries, animations, או כשיש הרבה סגנונות - במקרים אלה עדיף CSS Modules או Tailwind.