לדלג לתוכן

8.4 מנטין קומפוננטות פתרון

פתרון - מנטין קומפוננטות

פתרון תרגיל 1

import {
  Paper,
  Title,
  TextInput,
  NumberInput,
  Select,
  Radio,
  Checkbox,
  Textarea,
  Switch,
  Button,
  Stack,
  Group,
} from "@mantine/core";
import { useState } from "react";

function RegistrationForm() {
  const [agreed, setAgreed] = useState(false);

  return (
    <Paper shadow="sm" p="xl" withBorder maw={500} mx="auto" mt="xl">
      <Title order={2} mb="lg">
        טופס רישום
      </Title>

      <Stack gap="md">
        <TextInput label="שם מלא" placeholder="הכנס שם מלא" required />

        <TextInput
          label="אימייל"
          placeholder="your@email.com"
          required
          type="email"
        />

        <NumberInput
          label="גיל"
          placeholder="הכנס גיל"
          min={13}
          max={120}
          required
        />

        <Select
          label="מדינה"
          placeholder="בחר מדינה"
          searchable
          data={[
            "ישראל",
            "ארצות הברית",
            "בריטניה",
            "גרמניה",
            "צרפת",
            "קנדה",
            "אוסטרליה",
          ]}
          nothingFoundMessage="לא נמצא"
          required
        />

        <Radio.Group label="מגדר" required>
          <Group mt="xs">
            <Radio value="male" label="זכר" />
            <Radio value="female" label="נקבה" />
            <Radio value="other" label="אחר" />
          </Group>
        </Radio.Group>

        <Textarea
          label="אודות"
          placeholder="ספר לנו על עצמך..."
          minRows={3}
          autosize
        />

        <Switch label="אני רוצה לקבל עדכונים באימייל" defaultChecked />

        <Checkbox
          label="אני מסכים לתנאי השימוש ומדיניות הפרטיות"
          checked={agreed}
          onChange={(event) => setAgreed(event.currentTarget.checked)}
        />

        <Button fullWidth disabled={!agreed} mt="md">
          הרשם
        </Button>
      </Stack>
    </Paper>
  );
}
  • הכפתור disabled כשה-Checkbox לא מסומן
  • Select עם searchable מאפשר חיפוש בתוך הרשימה
  • NumberInput עם min/max מגביל את הערכים

פתרון תרגיל 2

import {
  Container,
  Title,
  TextInput,
  Select,
  SimpleGrid,
  Card,
  Image,
  Text,
  Badge,
  Button,
  Group,
  Pagination,
  Stack,
} from "@mantine/core";
import { useState } from "react";

interface Product {
  id: number;
  name: string;
  description: string;
  price: number;
  category: string;
  badge?: string;
  image: string;
}

const products: Product[] = [
  {
    id: 1,
    name: "אוזניות Bluetooth",
    description: "אוזניות אלחוטיות עם ביטול רעשים",
    price: 299,
    category: "אלקטרוניקה",
    badge: "מבצע",
    image: "https://placehold.co/300x200/eee/999?text=Headphones",
  },
  {
    id: 2,
    name: "מקלדת מכנית",
    description: "מקלדת גיימינג עם תאורת RGB",
    price: 449,
    category: "אלקטרוניקה",
    image: "https://placehold.co/300x200/eee/999?text=Keyboard",
  },
  {
    id: 3,
    name: "חולצת ספורט",
    description: "חולצה נושמת לפעילות ספורטיבית",
    price: 89,
    category: "ביגוד",
    badge: "חדש",
    image: "https://placehold.co/300x200/eee/999?text=Shirt",
  },
  {
    id: 4,
    name: "ספר TypeScript",
    description: "מדריך מקיף ל-TypeScript למתחילים ומתקדמים",
    price: 120,
    category: "ספרים",
    image: "https://placehold.co/300x200/eee/999?text=Book",
  },
];

function ProductsPage() {
  const [search, setSearch] = useState("");
  const [category, setCategory] = useState<string | null>(null);
  const [page, setPage] = useState(1);

  const filtered = products.filter((p) => {
    const matchesSearch = p.name.includes(search);
    const matchesCategory = !category || p.category === category;
    return matchesSearch && matchesCategory;
  });

  return (
    <Container size="lg" py="xl">
      <Title order={2} mb="lg">
        המוצרים שלנו
      </Title>

      {/* סינון */}
      <Group mb="lg">
        <TextInput
          placeholder="חפש מוצרים..."
          value={search}
          onChange={(e) => setSearch(e.currentTarget.value)}
          style={{ flex: 1 }}
        />
        <Select
          placeholder="קטגוריה"
          clearable
          value={category}
          onChange={setCategory}
          data={["אלקטרוניקה", "ביגוד", "ספרים"]}
          w={200}
        />
      </Group>

      {/* רשת מוצרים */}
      <SimpleGrid cols={{ base: 1, sm: 2, md: 3, lg: 4 }} spacing="lg">
        {filtered.map((product) => (
          <Card key={product.id} shadow="sm" padding="lg" radius="md" withBorder>
            <Card.Section>
              <Image src={product.image} height={160} alt={product.name} />
            </Card.Section>

            <Group justify="space-between" mt="md" mb="xs">
              <Text fw={500} lineClamp={1}>
                {product.name}
              </Text>
              {product.badge && (
                <Badge color="pink" variant="light">
                  {product.badge}
                </Badge>
              )}
            </Group>

            <Text size="sm" c="dimmed" lineClamp={2} mb="md">
              {product.description}
            </Text>

            <Group justify="space-between" align="center">
              <Text size="lg" fw={700}>
                {product.price} ש
              </Text>
              <Button variant="light" size="sm">
                הוסף לעגלה
              </Button>
            </Group>
          </Card>
        ))}
      </SimpleGrid>

      {/* עימוד */}
      <Group justify="center" mt="xl">
        <Pagination value={page} onChange={setPage} total={3} />
      </Group>
    </Container>
  );
}

פתרון תרגיל 3

import {
  Container,
  Title,
  Tabs,
  TextInput,
  Select,
  Switch,
  Button,
  Stack,
  Group,
  Paper,
  Divider,
  Text,
} from "@mantine/core";

function SettingsPage() {
  return (
    <Container size="sm" py="xl">
      <Title order={2} mb="lg">
        הגדרות
      </Title>

      <Paper shadow="xs" withBorder>
        <Tabs defaultValue="general">
          <Tabs.List>
            <Tabs.Tab value="general">כללי</Tabs.Tab>
            <Tabs.Tab value="security">אבטחה</Tabs.Tab>
            <Tabs.Tab value="notifications">התראות</Tabs.Tab>
          </Tabs.List>

          {/* כללי */}
          <Tabs.Panel value="general" p="lg">
            <Stack gap="md">
              <TextInput label="שם מלא" defaultValue="ישראל ישראלי" />
              <TextInput label="אימייל" defaultValue="israel@example.com" />
              <Select
                label="שפה"
                defaultValue="he"
                data={[
                  { value: "he", label: "עברית" },
                  { value: "en", label: "English" },
                  { value: "ar", label: "العربية" },
                ]}
              />
              <Select
                label="אזור זמן"
                defaultValue="asia-jerusalem"
                data={[
                  { value: "asia-jerusalem", label: "ישראל (UTC+2)" },
                  { value: "europe-london", label: "לונדון (UTC+0)" },
                  { value: "america-new_york", label: "ניו יורק (UTC-5)" },
                ]}
              />
              <Group justify="flex-end" mt="md">
                <Button>שמור שינויים</Button>
              </Group>
            </Stack>
          </Tabs.Panel>

          {/* אבטחה */}
          <Tabs.Panel value="security" p="lg">
            <Stack gap="md">
              <TextInput
                label="סיסמה נוכחית"
                type="password"
                placeholder="הכנס סיסמה נוכחית"
              />
              <TextInput
                label="סיסמה חדשה"
                type="password"
                placeholder="הכנס סיסמה חדשה"
              />
              <TextInput
                label="אישור סיסמה"
                type="password"
                placeholder="הכנס שוב את הסיסמה החדשה"
              />
              <Divider />
              <Switch
                label="אימות דו-שלבי"
                description="שכבת אבטחה נוספת עם קוד SMS"
              />
              <Group justify="flex-end" mt="md">
                <Button>עדכן סיסמה</Button>
              </Group>
            </Stack>
          </Tabs.Panel>

          {/* התראות */}
          <Tabs.Panel value="notifications" p="lg">
            <Stack gap="md">
              <Text fw={500} mb="xs">ערוצי התראות</Text>
              <Switch label="התראות אימייל" defaultChecked />
              <Switch label="התראות SMS" />
              <Switch label="התראות Push" defaultChecked />
              <Switch label="עדכוני מוצר" defaultChecked />
              <Divider />
              <Select
                label="תדירות עדכונים"
                defaultValue="daily"
                data={[
                  { value: "immediate", label: "מיידי" },
                  { value: "daily", label: "סיכום יומי" },
                  { value: "weekly", label: "סיכום שבועי" },
                ]}
              />
              <Group justify="flex-end" mt="md">
                <Button>שמור העדפות</Button>
              </Group>
            </Stack>
          </Tabs.Panel>
        </Tabs>
      </Paper>
    </Container>
  );
}

פתרון תרגיל 4

import {
  Container,
  Title,
  Table,
  Badge,
  Avatar,
  Group,
  Button,
  Modal,
  TextInput,
  Select,
  Radio,
  Textarea,
  Stack,
  Menu,
  Tabs,
  Text,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { useState } from "react";

interface Task {
  id: number;
  name: string;
  assignee: string;
  dueDate: string;
  status: "open" | "in-progress" | "done";
  priority: "low" | "medium" | "high";
}

const tasks: Task[] = [
  { id: 1, name: "עיצוב דף הבית", assignee: "דנה", dueDate: "2024-03-15", status: "in-progress", priority: "high" },
  { id: 2, name: "פיתוח API", assignee: "יוסי", dueDate: "2024-03-20", status: "open", priority: "high" },
  { id: 3, name: "כתיבת בדיקות", assignee: "אבי", dueDate: "2024-03-25", status: "open", priority: "medium" },
  { id: 4, name: "תיעוד", assignee: "שירה", dueDate: "2024-03-30", status: "done", priority: "low" },
];

const statusConfig: Record<string, { color: string; label: string }> = {
  open: { color: "blue", label: "פתוח" },
  "in-progress": { color: "yellow", label: "בתהליך" },
  done: { color: "green", label: "הושלם" },
};

const priorityConfig: Record<string, { color: string; label: string }> = {
  low: { color: "gray", label: "נמוכה" },
  medium: { color: "orange", label: "בינונית" },
  high: { color: "red", label: "גבוהה" },
};

function TaskManager() {
  const [opened, { open, close }] = useDisclosure(false);
  const [activeTab, setActiveTab] = useState<string | null>("all");

  const filteredTasks = tasks.filter((task) => {
    if (activeTab === "all") return true;
    return task.status === activeTab;
  });

  return (
    <Container size="lg" py="xl">
      <Group justify="space-between" mb="lg">
        <Title order={2}>ניהול משימות</Title>
        <Button onClick={open}>הוסף משימה</Button>
      </Group>

      <Tabs value={activeTab} onChange={setActiveTab} mb="lg">
        <Tabs.List>
          <Tabs.Tab value="all">הכל ({tasks.length})</Tabs.Tab>
          <Tabs.Tab value="open">
            פתוחות ({tasks.filter((t) => t.status === "open").length})
          </Tabs.Tab>
          <Tabs.Tab value="in-progress">
            בתהליך ({tasks.filter((t) => t.status === "in-progress").length})
          </Tabs.Tab>
          <Tabs.Tab value="done">
            הושלמו ({tasks.filter((t) => t.status === "done").length})
          </Tabs.Tab>
        </Tabs.List>
      </Tabs>

      <Table striped highlightOnHover withTableBorder>
        <Table.Thead>
          <Table.Tr>
            <Table.Th>משימה</Table.Th>
            <Table.Th>אחראי</Table.Th>
            <Table.Th>תאריך יעד</Table.Th>
            <Table.Th>סטטוס</Table.Th>
            <Table.Th>עדיפות</Table.Th>
            <Table.Th>פעולות</Table.Th>
          </Table.Tr>
        </Table.Thead>
        <Table.Tbody>
          {filteredTasks.map((task) => (
            <Table.Tr key={task.id}>
              <Table.Td>
                <Text size="sm" fw={500}>{task.name}</Text>
              </Table.Td>
              <Table.Td>
                <Group gap="xs">
                  <Avatar size="sm" color="blue" radius="xl">
                    {task.assignee.charAt(0)}
                  </Avatar>
                  <Text size="sm">{task.assignee}</Text>
                </Group>
              </Table.Td>
              <Table.Td>
                <Text size="sm">{task.dueDate}</Text>
              </Table.Td>
              <Table.Td>
                <Badge color={statusConfig[task.status].color} variant="light">
                  {statusConfig[task.status].label}
                </Badge>
              </Table.Td>
              <Table.Td>
                <Badge color={priorityConfig[task.priority].color} variant="dot">
                  {priorityConfig[task.priority].label}
                </Badge>
              </Table.Td>
              <Table.Td>
                <Menu shadow="md" width={150}>
                  <Menu.Target>
                    <Button variant="subtle" size="xs">...</Button>
                  </Menu.Target>
                  <Menu.Dropdown>
                    <Menu.Item>עריכה</Menu.Item>
                    <Menu.Item>שינוי סטטוס</Menu.Item>
                    <Menu.Divider />
                    <Menu.Item color="red">מחיקה</Menu.Item>
                  </Menu.Dropdown>
                </Menu>
              </Table.Td>
            </Table.Tr>
          ))}
        </Table.Tbody>
      </Table>

      <Modal opened={opened} onClose={close} title="הוספת משימה חדשה" centered>
        <Stack>
          <TextInput label="שם המשימה" placeholder="הכנס שם" required />
          <Select
            label="אחראי"
            placeholder="בחר אחראי"
            data={["דנה", "יוסי", "אבי", "שירה"]}
            required
          />
          <TextInput label="תאריך יעד" placeholder="YYYY-MM-DD" required />
          <Radio.Group label="עדיפות" defaultValue="medium">
            <Group mt="xs">
              <Radio value="low" label="נמוכה" />
              <Radio value="medium" label="בינונית" />
              <Radio value="high" label="גבוהה" />
            </Group>
          </Radio.Group>
          <Textarea label="תיאור" placeholder="תיאור המשימה..." minRows={3} />
          <Group justify="flex-end" mt="md">
            <Button variant="default" onClick={close}>ביטול</Button>
            <Button onClick={close}>הוסף</Button>
          </Group>
        </Stack>
      </Modal>
    </Container>
  );
}

פתרון תרגיל 5

import {
  Container,
  Paper,
  Avatar,
  Title,
  Text,
  Group,
  Button,
  Stack,
  Tabs,
  Card,
  SimpleGrid,
  Alert,
  Progress,
  Divider,
} from "@mantine/core";

function ProfilePage() {
  return (
    <Container size="md" py="xl">
      {/* חלק עליון - פרטי משתמש */}
      <Paper shadow="sm" p="xl" withBorder mb="lg">
        <Group justify="space-between" align="flex-start">
          <Group>
            <Avatar size={80} color="blue" radius="xl">
              יכ
            </Avatar>
            <div>
              <Title order={3}>יוסי כהן</Title>
              <Text c="dimmed" size="sm">מפתח Frontend בכיר</Text>
            </div>
          </Group>
          <Group>
            <Button variant="outline" size="sm">עריכה</Button>
            <Button variant="light" size="sm">שלח הודעה</Button>
          </Group>
        </Group>

        <Divider my="lg" />

        <Group justify="center" gap={60}>
          <Stack align="center" gap={0}>
            <Text size="xl" fw={700}>42</Text>
            <Text size="sm" c="dimmed">פוסטים</Text>
          </Stack>
          <Stack align="center" gap={0}>
            <Text size="xl" fw={700}>1,234</Text>
            <Text size="sm" c="dimmed">עוקבים</Text>
          </Stack>
          <Stack align="center" gap={0}>
            <Text size="xl" fw={700}>567</Text>
            <Text size="sm" c="dimmed">עוקב</Text>
          </Stack>
        </Group>
      </Paper>

      {/* חלק אמצעי - תוכן */}
      <Paper shadow="sm" withBorder mb="lg">
        <Tabs defaultValue="posts">
          <Tabs.List>
            <Tabs.Tab value="posts">פוסטים</Tabs.Tab>
            <Tabs.Tab value="comments">תגובות</Tabs.Tab>
            <Tabs.Tab value="favorites">מועדפים</Tabs.Tab>
          </Tabs.List>

          <Tabs.Panel value="posts" p="md">
            <SimpleGrid cols={{ base: 1, sm: 2 }}>
              {[1, 2, 3, 4].map((n) => (
                <Card key={n} shadow="xs" p="md" withBorder>
                  <Text fw={500} mb="xs">פוסט מספר {n}</Text>
                  <Text size="sm" c="dimmed" lineClamp={2}>
                    תוכן הפוסט מספר {n}. זהו תיאור קצר של הפוסט שנכתב.
                  </Text>
                  <Text size="xs" c="dimmed" mt="sm">לפני {n} ימים</Text>
                </Card>
              ))}
            </SimpleGrid>
          </Tabs.Panel>

          <Tabs.Panel value="comments" p="md">
            <Stack>
              {[1, 2, 3].map((n) => (
                <Paper key={n} p="md" bg="gray.0" radius="md">
                  <Text size="sm" fw={500} mb={4}>בתגובה לפוסט "כותרת {n}"</Text>
                  <Text size="sm" c="dimmed">תגובה לדוגמה מספר {n}</Text>
                </Paper>
              ))}
            </Stack>
          </Tabs.Panel>

          <Tabs.Panel value="favorites" p="md">
            <SimpleGrid cols={{ base: 1, sm: 2 }}>
              {[1, 2].map((n) => (
                <Card key={n} shadow="xs" p="md" withBorder>
                  <Text fw={500} mb="xs">מועדף {n}</Text>
                  <Text size="sm" c="dimmed">פוסט שנשמר כמועדף</Text>
                </Card>
              ))}
            </SimpleGrid>
          </Tabs.Panel>
        </Tabs>
      </Paper>

      {/* חלק תחתון */}
      <Stack gap="md">
        <Alert variant="light" color="blue" title="מידע על החשבון">
          החשבון שלך פעיל מאז ינואר 2022. תוכנית נוכחית: פרו.
        </Alert>

        <Paper shadow="sm" p="lg" withBorder>
          <Group justify="space-between" mb="xs">
            <Text size="sm" fw={500}>השלמת פרופיל</Text>
            <Text size="sm" c="dimmed">75%</Text>
          </Group>
          <Progress value={75} size="lg" radius="xl" />
          <Text size="xs" c="dimmed" mt="xs">
            הוסף תמונת פרופיל ותיאור להשלמת 100%
          </Text>
        </Paper>
      </Stack>
    </Container>
  );
}

פתרון תרגיל 6

import {
  Drawer,
  Button,
  Stack,
  Group,
  Text,
  NumberInput,
  Divider,
  Alert,
  Skeleton,
  Paper,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { useState } from "react";

interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

function ShoppingCart() {
  const [opened, { open, close }] = useDisclosure(false);
  const [loading, setLoading] = useState(false);
  const [items, setItems] = useState<CartItem[]>([
    { id: 1, name: "אוזניות Bluetooth", price: 299, quantity: 1 },
    { id: 2, name: "מקלדת מכנית", price: 449, quantity: 1 },
    { id: 3, name: "עכבר אלחוטי", price: 179, quantity: 2 },
  ]);

  function updateQuantity(id: number, quantity: number) {
    setItems((prev) =>
      prev.map((item) => (item.id === id ? { ...item, quantity } : item))
    );
  }

  function removeItem(id: number) {
    setItems((prev) => prev.filter((item) => item.id !== id));
  }

  const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);

  return (
    <>
      <Drawer
        opened={opened}
        onClose={close}
        title="העגלה שלך"
        position="left"
        size="md"
      >
        {loading ? (
          <Stack gap="md">
            {[1, 2, 3].map((n) => (
              <Paper key={n} p="md" withBorder>
                <Group>
                  <Skeleton height={20} width="40%" />
                  <Skeleton height={20} width="20%" />
                </Group>
                <Skeleton height={36} mt="sm" width="30%" />
              </Paper>
            ))}
            <Divider />
            <Skeleton height={24} width="50%" />
            <Skeleton height={40} />
          </Stack>
        ) : items.length === 0 ? (
          <Alert variant="light" color="blue" title="העגלה ריקה">
            לא הוספת מוצרים לעגלה עדיין. חזור לחנות והוסף מוצרים.
          </Alert>
        ) : (
          <Stack gap="md">
            {items.map((item) => (
              <Paper key={item.id} p="md" withBorder>
                <Group justify="space-between" mb="xs">
                  <Text fw={500}>{item.name}</Text>
                  <Text fw={700}>{item.price} ש"ח</Text>
                </Group>
                <Group>
                  <NumberInput
                    value={item.quantity}
                    onChange={(val) => updateQuantity(item.id, Number(val) || 1)}
                    min={1}
                    max={99}
                    w={100}
                    size="xs"
                  />
                  <Button
                    variant="subtle"
                    color="red"
                    size="xs"
                    onClick={() => removeItem(item.id)}
                  >
                    הסר
                  </Button>
                </Group>
              </Paper>
            ))}

            <Divider />

            <Group justify="space-between">
              <Text size="lg" fw={700}>
                סה"כ:
              </Text>
              <Text size="xl" fw={700} c="blue">
                {total} ש
              </Text>
            </Group>

            <Button fullWidth size="lg">
              המשך לתשלום
            </Button>
          </Stack>
        )}
      </Drawer>

      <Button onClick={open}>
        עגלה ({items.length})
      </Button>
    </>
  );
}
  • הדראוור פותח מצד שמאל (position="left")
  • שלושה מצבים: טעינה (Skeleton), ריק (Alert), עם מוצרים
  • NumberInput לניהול כמות עם min/max

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

  1. Modal לעומת Drawer - Modal הוא חלון שצף במרכז המסך ומשמש לפעולות ממוקדות כמו טפסים, אישורים ודיאלוגים. Drawer הוא מגירה שנפתחת מצד המסך ומשמשת לתוכן ארוך יותר כמו תפריטי ניווט, עגלת קניות או פאנל סינון. ככלל - Modal לאינטראקציה קצרה, Drawer לתוכן ארוך.

  2. Card.Section - Card.Section הוא אלמנט שמתפרס על כל רוחב הכרטיסייה, כולל מעבר ל-padding של הכרטיס. זה שימושי בעיקר לתמונות שרוצים שיהיו edge-to-edge (מקצה לקצה) בתוך הכרטיס, ולמפרידים (dividers) שחוצים את כל הרוחב.

  3. Notification לעומת Alert - Notification היא הודעה זמנית שבדרך כלל נעלמת אחרי כמה שניות (toast). Alert הוא רכיב שמוצג כחלק מהדף ונשאר עד שהמשתמש סוגר אותו או עד שהמצב משתנה. נשתמש ב-Notification לאירועים חולפים (שמירה הצליחה) וב-Alert למידע שצריך להישאר (אזהרת אבטחה).

  4. LoadingOverlay - הקומפוננטה ממוקמת עם position: absolute ומכסה את כל ההורה שלה. לכן חובה שהאלמנט ההורה יהיה position: relative (ב-Mantine: pos="relative"). בנוסף צריך mih (min-height) על ההורה כדי שה-overlay יהיה גלוי.

  5. יתרון Menu - Menu של Mantine מגיע עם: מיקום אוטומטי (לא חורג מהמסך), נגישות מלאה (ניווט מקלדת, ARIA), אנימציות כניסה/יציאה, סגירה בלחיצה בחוץ, תמיכה ב-groups/labels/dividers, ו-TypeScript מלא. לבנות את כל זה מאפס דורש מאות שורות קוד.