לדלג לתוכן

8.3 מנטין בסיסי פתרון

פתרון - מנטין בסיסי

פתרון תרגיל 1

npm create vite@latest my-mantine-app -- --template react-ts
cd my-mantine-app
npm install @mantine/core @mantine/hooks
npm install -D postcss postcss-preset-mantine postcss-simple-vars
// postcss.config.cjs
module.exports = {
  plugins: {
    "postcss-preset-mantine": {},
    "postcss-simple-vars": {
      variables: {
        "mantine-breakpoint-xs": "36em",
        "mantine-breakpoint-sm": "48em",
        "mantine-breakpoint-md": "62em",
        "mantine-breakpoint-lg": "75em",
        "mantine-breakpoint-xl": "88em",
      },
    },
  },
};
<!-- index.html - הוספת פונטים -->
<head>
  <link
    href="https://fonts.googleapis.com/css2?family=Heebo:wght@400;500;700&family=Rubik:wght@400;500;700&display=swap"
    rel="stylesheet"
  />
</head>
// main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { createTheme, MantineProvider } from "@mantine/core";
import "@mantine/core/styles.css";
import App from "./App";

const theme = createTheme({
  primaryColor: "green",
  fontFamily: "Rubik, sans-serif",
  defaultRadius: "md",
  headings: {
    fontFamily: "Heebo, sans-serif",
    fontWeight: "700",
  },
});

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <MantineProvider theme={theme}>
      <App />
    </MantineProvider>
  </StrictMode>
);
// App.tsx
import { Button, Title, Text, Group, Stack, Container, Paper } from "@mantine/core";

function App() {
  return (
    <Container p="xl">
      <Stack gap="lg">
        <Title order={1}>בדיקת Mantine</Title>
        <Text size="lg" c="dimmed">
          הפונט הזה הוא Rubik, והכותרת ב-Heebo
        </Text>
        <Group>
          <Button>כפתור ראשי (ירוק)</Button>
          <Button variant="outline">כפתור מתאר</Button>
          <Button variant="light">כפתור בהיר</Button>
        </Group>
        <Paper shadow="sm" p="lg" withBorder>
          <Text>כרטיס עם Paper</Text>
        </Paper>
      </Stack>
    </Container>
  );
}

export default App;
  • הפונט Rubik ישמש לכל הטקסטים, ו-Heebo לכותרות
  • הצבע הראשי ירוק - כל הכפתורים ורכיבים אחרים יהיו ירוקים כברירת מחדל
  • הרדיוס md ייתן פינות מעוגלות מתונות לכל הקומפוננטות

פתרון תרגיל 2

import { Paper, Group, Stack, Text, Title, Button, Avatar } from "@mantine/core";

interface UserCardProps {
  name: string;
  role: string;
  avatarUrl?: string;
  onMessage?: () => void;
  onProfile?: () => void;
}

function UserCard({ name, role, avatarUrl, onMessage, onProfile }: UserCardProps) {
  return (
    <Paper shadow="sm" p="lg" withBorder radius="md" maw={320}>
      <Stack align="center" gap="md">
        <Avatar src={avatarUrl} size={80} radius="xl" color="blue">
          {name.charAt(0)}
        </Avatar>
        <div style={{ textAlign: "center" }}>
          <Title order={4}>{name}</Title>
          <Text size="sm" c="dimmed" mt={4}>
            {role}
          </Text>
        </div>
        <Group gap="sm" w="100%">
          <Button variant="light" flex={1} onClick={onMessage}>
            שלח הודעה
          </Button>
          <Button variant="outline" flex={1} onClick={onProfile}>
            פרופיל
          </Button>
        </Group>
      </Stack>
    </Paper>
  );
}

// שימוש
function App() {
  return (
    <Group p="xl" justify="center">
      <UserCard name="יוסי כהן" role="מפתח Frontend" />
      <UserCard name="דנה לוי" role="מעצבת UX" />
      <UserCard name="אבי מזרחי" role="מנהל פרויקט" />
    </Group>
  );
}
  • Avatar מציג את האות הראשונה של השם כשאין תמונה
  • flex={1} על הכפתורים גורם להם לחלוק את הרוחב שווה
  • maw (max-width) מגביל את רוחב הכרטיס

פתרון תרגיל 3

import {
  Container,
  Title,
  Text,
  SimpleGrid,
  Grid,
  Paper,
  Group,
  Stack,
} from "@mantine/core";

interface Stat {
  title: string;
  value: string;
  change: number;
}

const stats: Stat[] = [
  { title: "משתמשים פעילים", value: "2,345", change: 12.5 },
  { title: "הזמנות החודש", value: "789", change: -3.2 },
  { title: "הכנסות", value: "156,789 ש\"ח", change: 18.7 },
  { title: "שיעור המרה", value: "4.8%", change: 2.1 },
];

const recentActivity = [
  "משתמש חדש נרשם - לפני 3 דקות",
  "הזמנה #1234 הושלמה - לפני 15 דקות",
  "תשלום התקבל מלקוח - לפני 30 דקות",
  "מוצר חדש נוסף - לפני שעה",
  "דוח חודשי הופק - לפני שעתיים",
];

function StatCard({ title, value, change }: Stat) {
  const isPositive = change > 0;

  return (
    <Paper shadow="xs" p="md" withBorder>
      <Text size="sm" c="dimmed" mb="xs">
        {title}
      </Text>
      <Group justify="space-between" align="flex-end">
        <Text size="xl" fw={700}>
          {value}
        </Text>
        <Text size="sm" c={isPositive ? "green" : "red"} fw={500}>
          {isPositive ? "+" : ""}
          {change}%
        </Text>
      </Group>
    </Paper>
  );
}

function StatsPage() {
  return (
    <Container fluid p="lg">
      <Title order={2} mb="lg">
        דשבורד - סטטיסטיקות
      </Title>

      {/* כרטיסי סטטיסטיקה */}
      <SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} mb="xl">
        {stats.map((stat) => (
          <StatCard key={stat.title} {...stat} />
        ))}
      </SimpleGrid>

      {/* תוכן נוסף */}
      <Grid>
        <Grid.Col span={{ base: 12, md: 8 }}>
          <Paper shadow="xs" p="lg" withBorder>
            <Title order={4} mb="md">
              פעילות אחרונה
            </Title>
            <Stack gap="sm">
              {recentActivity.map((activity, index) => (
                <Group key={index} gap="sm">
                  <Text size="sm" c="dimmed">
                    {index + 1}.
                  </Text>
                  <Text size="sm">{activity}</Text>
                </Group>
              ))}
            </Stack>
          </Paper>
        </Grid.Col>

        <Grid.Col span={{ base: 12, md: 4 }}>
          <Paper shadow="xs" p="lg" withBorder>
            <Title order={4} mb="md">
              סיכום
            </Title>
            <Stack gap="md">
              <div>
                <Text size="sm" c="dimmed">סה"כ הכנסות השנה</Text>
                <Text size="lg" fw={700}>1,234,567 ש"ח</Text>
              </div>
              <div>
                <Text size="sm" c="dimmed">ממוצע חודשי</Text>
                <Text size="lg" fw={700}>102,880 ש"ח</Text>
              </div>
              <div>
                <Text size="sm" c="dimmed">יעד שנתי</Text>
                <Text size="lg" fw={700}>1,500,000 ש"ח</Text>
              </div>
            </Stack>
          </Paper>
        </Grid.Col>
      </Grid>
    </Container>
  );
}
  • SimpleGrid מתאים לסטטיסטיקות כי כולן באותו גודל
  • Grid מתאים לתוכן עם פרופורציות שונות (8/12 ו-4/12)
  • שינוי צבע בהתאם לערך חיובי/שלילי

פתרון תרגיל 4

import {
  AppShell,
  Burger,
  Group,
  NavLink,
  Title,
  Text,
  SimpleGrid,
  Paper,
  Stack,
  Button,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";

const navItems = [
  { label: "דשבורד", active: true },
  { label: "משתמשים" },
  { label: "מוצרים" },
  { label: "הזמנות" },
  {
    label: "ניהול",
    children: [
      { label: "תפקידים" },
      { label: "הרשאות" },
      { label: "הגדרות מערכת" },
    ],
  },
];

const cards = [
  { title: "משתמשים חדשים", value: "23", description: "השבוע" },
  { title: "הזמנות פתוחות", value: "12", description: "ממתינות לטיפול" },
  { title: "הכנסות היום", value: "4,567 ש\"ח", description: "עד כה" },
  { title: "פניות תמיכה", value: "8", description: "ללא מענה" },
  { title: "מוצרים פעילים", value: "156", description: "בקטלוג" },
  { title: "ביקורים", value: "1,234", description: "היום" },
];

function AppLayout() {
  const [opened, { toggle }] = useDisclosure();

  return (
    <AppShell
      header={{ height: 60 }}
      navbar={{ width: 260, breakpoint: "sm", collapsed: { mobile: !opened } }}
      padding="md"
    >
      <AppShell.Header bg="blue.9" c="white">
        <Group h="100%" px="md" justify="space-between">
          <Group>
            <Burger
              opened={opened}
              onClick={toggle}
              hiddenFrom="sm"
              size="sm"
              color="white"
            />
            <Title order={3} c="white">
              ניהול חנות
            </Title>
          </Group>
          <Group gap="md" visibleFrom="sm">
            <Button variant="light" color="white" size="xs">
              עזרה
            </Button>
            <Button variant="white" color="blue" size="xs">
              התנתק
            </Button>
          </Group>
        </Group>
      </AppShell.Header>

      <AppShell.Navbar p="md">
        <Stack gap="xs">
          {navItems.map((item) => (
            <NavLink
              key={item.label}
              label={item.label}
              active={item.active}
              childrenOffset={28}
            >
              {item.children?.map((child) => (
                <NavLink key={child.label} label={child.label} />
              ))}
            </NavLink>
          ))}
        </Stack>
      </AppShell.Navbar>

      <AppShell.Main>
        <Title order={2} mb="lg">
          דשבורד ראשי
        </Title>
        <SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }}>
          {cards.map((card) => (
            <Paper key={card.title} shadow="xs" p="md" withBorder>
              <Text size="sm" c="dimmed">
                {card.title}
              </Text>
              <Text size="xl" fw={700} my="xs">
                {card.value}
              </Text>
              <Text size="xs" c="dimmed">
                {card.description}
              </Text>
            </Paper>
          ))}
        </SimpleGrid>
      </AppShell.Main>
    </AppShell>
  );
}
  • ה-Header מעוצב עם bg="blue.9" ו-c="white" לרקע כחול כהה וטקסט לבן
  • NavLink עם children יוצר תת-תפריט אוטומטית
  • useDisclosure מנהל את מצב פתיחה/סגירה של הסיידבר
  • visibleFrom="sm" מסתיר כפתורים בטלפון

פתרון תרגיל 5

import {
  Container,
  Title,
  Text,
  SimpleGrid,
  Paper,
  Button,
  Stack,
} from "@mantine/core";

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

const features: Feature[] = [
  {
    icon: ">_",
    title: "פיתוח מהיר",
    description:
      "כלים מתקדמים שמאפשרים פיתוח מהיר ויעיל. חסכו שעות עבודה עם תבניות מוכנות וקומפוננטות שימושיות.",
  },
  {
    icon: "#",
    title: "ביצועים מעולים",
    description:
      "ארכיטקטורה מותאמת לביצועים. אופטימיזציה אוטומטית, caching חכם ו-lazy loading מובנה.",
  },
  {
    icon: "{}",
    title: "קוד נקי",
    description:
      "API אינטואיטיבי שמעודד כתיבת קוד נקי וקריא. TypeScript מלא עם השלמה אוטומטית.",
  },
];

function FeatureSection() {
  return (
    <Container size="lg" py="xl">
      <Stack align="center" gap="xs" mb="xl">
        <Title order={2} ta="center">
          למה לבחור בנו
        </Title>
        <Text size="lg" c="dimmed" ta="center" maw={500}>
          הפלטפורמה שלנו מספקת את כל הכלים שאתם צריכים לבניית אפליקציות מודרניות
        </Text>
      </Stack>

      <SimpleGrid cols={{ base: 1, sm: 3 }} spacing="lg">
        {features.map((feature) => (
          <Paper
            key={feature.title}
            shadow="xs"
            p="xl"
            withBorder
            radius="md"
          >
            <Stack gap="md">
              <Text size="2rem" fw={700} c="blue">
                {feature.icon}
              </Text>
              <Title order={4}>{feature.title}</Title>
              <Text size="sm" c="dimmed" lh={1.6}>
                {feature.description}
              </Text>
              <Button variant="subtle" size="sm" p={0}>
                למד עוד
              </Button>
            </Stack>
          </Paper>
        ))}
      </SimpleGrid>
    </Container>
  );
}
  • Container size="lg" מגביל את רוחב האזור
  • Stack align="center" ממרכז את הכותרות
  • maw (max-width) על הטקסט מונע שורות ארוכות מדי
  • lh (line-height) על הטקסט משפר קריאות

פתרון תרגיל 6

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

function LandingPage() {
  return (
    <div>
      {/* Hero */}
      <Container size="md" py={80}>
        <Stack align="center" gap="lg">
          <Title order={1} ta="center" size="3rem">
            בנו אפליקציות מדהימות בזמן שיא
          </Title>
          <Text size="xl" c="dimmed" ta="center" maw={600}>
            פלטפורמה מודרנית לפיתוח ממשקי משתמש מהירים, יפים ונגישים
          </Text>
          <Group mt="md">
            <Button size="lg">התחל בחינם</Button>
            <Button size="lg" variant="outline">
              צפה בהדגמה
            </Button>
          </Group>
        </Stack>
      </Container>

      {/* תכונות */}
      <Container size="lg" py="xl" bg="gray.0">
        <Title order={2} ta="center" mb="xl">
          הכלים שלנו
        </Title>
        <SimpleGrid cols={{ base: 1, sm: 2, md: 4 }} spacing="lg">
          {[
            { title: "קומפוננטות", desc: "מעל 100 קומפוננטות מוכנות" },
            { title: "הוקים", desc: "הוקים שימושיים לכל צורך" },
            { title: "TypeScript", desc: "תמיכה מלאה ב-TypeScript" },
            { title: "נגישות", desc: "עמידה בתקני WCAG" },
          ].map((feature) => (
            <Paper key={feature.title} shadow="xs" p="lg" withBorder>
              <Title order={4} mb="xs">
                {feature.title}
              </Title>
              <Text size="sm" c="dimmed">
                {feature.desc}
              </Text>
            </Paper>
          ))}
        </SimpleGrid>
      </Container>

      {/* Pricing */}
      <Container size="lg" py={60}>
        <Title order={2} ta="center" mb="xl">
          תוכניות מחיר
        </Title>
        <SimpleGrid cols={{ base: 1, sm: 3 }} spacing="lg">
          {/* בסיסי */}
          <Paper shadow="xs" p="xl" withBorder>
            <Stack>
              <Title order={3}>בסיסי</Title>
              <Group gap={4} align="flex-end">
                <Text size="2.5rem" fw={700}>חינם</Text>
              </Group>
              <Divider />
              <Stack gap="xs">
                <Text size="sm">V עד 3 פרויקטים</Text>
                <Text size="sm">V קהילה</Text>
                <Text size="sm" c="dimmed">X תמיכה מועדפת</Text>
              </Stack>
              <Button variant="default" fullWidth mt="md">
                התחל
              </Button>
            </Stack>
          </Paper>

          {/* פרו - מודגש */}
          <Paper shadow="xl" p="xl" withBorder style={{ border: "2px solid var(--mantine-color-blue-5)" }}>
            <Stack>
              <Group justify="space-between">
                <Title order={3}>פרו</Title>
                <Text size="xs" c="white" bg="blue" px="sm" py={4} style={{ borderRadius: 12 }}>
                  פופולרי
                </Text>
              </Group>
              <Group gap={4} align="flex-end">
                <Text size="2.5rem" fw={700}>49</Text>
                <Text size="sm" c="dimmed" mb={6}>ש"ח / חודש</Text>
              </Group>
              <Divider />
              <Stack gap="xs">
                <Text size="sm">V פרויקטים ללא הגבלה</Text>
                <Text size="sm">V תמיכה מועדפת</Text>
                <Text size="sm">V API גישה</Text>
              </Stack>
              <Button fullWidth mt="md">
                בחר תוכנית
              </Button>
            </Stack>
          </Paper>

          {/* ארגוני */}
          <Paper shadow="xs" p="xl" withBorder>
            <Stack>
              <Title order={3}>ארגוני</Title>
              <Group gap={4} align="flex-end">
                <Text size="2.5rem" fw={700}>149</Text>
                <Text size="sm" c="dimmed" mb={6}>ש"ח / חודש</Text>
              </Group>
              <Divider />
              <Stack gap="xs">
                <Text size="sm">V הכל בפרו</Text>
                <Text size="sm">V SSO</Text>
                <Text size="sm">V SLA מובטח</Text>
              </Stack>
              <Button variant="default" fullWidth mt="md">
                צור קשר
              </Button>
            </Stack>
          </Paper>
        </SimpleGrid>
      </Container>

      {/* Footer */}
      <Paper bg="gray.9" p="xl" radius={0}>
        <Container size="lg">
          <Group justify="space-between">
            <Text c="white" fw={700}>
              האפליקציה שלנו
            </Text>
            <Group gap="lg">
              <Text c="gray.4" size="sm" component="a" href="#">
                תנאי שימוש
              </Text>
              <Text c="gray.4" size="sm" component="a" href="#">
                פרטיות
              </Text>
              <Text c="gray.4" size="sm" component="a" href="#">
                צור קשר
              </Text>
            </Group>
          </Group>
        </Container>
      </Paper>
    </div>
  );
}
  • כל הדף בנוי עם Mantine בלבד ללא CSS מותאם
  • כרטיס הפרו מודגש עם shadow="xl" ו-border צבעוני
  • component="a" הופך Text לקישור (polymorphic component)

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

  1. SimpleGrid לעומת Grid - SimpleGrid מחלק את המרחב שווה בשווה בין כל הילדים, בלי אפשרות לשנות את גודל כל עמודה בנפרד. Grid מאפשר שליטה מלאה עם span (מתוך 12 עמודות), offset, ו-order. נשתמש ב-SimpleGrid לרשתות אחידות (כרטיסים, תכונות) וב-Grid כשצריך פרופורציות שונות (סיידבר + תוכן).

  2. System Props - אלו תכונות CSS שאפשר להעביר ישירות כ-props לכל קומפוננטה של Mantine (כמו p, m, bg, c, w, h). הם חוסכים את הצורך לכתוב style={{ padding: "16px" }} או ליצור CSS class רק בשביל margin. במקום זה כותבים פשוט <Box p="md" mt="lg">.

  3. יתרון AppShell - AppShell מספק מבנה אפליקציה מלא עם header, sidebar, footer ותוכן שכבר מטפל ב: position fixed של ה-header, margin/padding נכון של התוכן, רספונסיביות של הסיידבר (המבורגר בטלפון), ו-spacing מתאים. לבנות את כל זה מאפס דורש הרבה CSS וחישובים.

  4. createTheme - הוא מגדיר ברירות מחדל גלובליות שחלות על כל הקומפוננטות. כש-primaryColor הוא green, כל Button ללא prop color יהיה ירוק. כש-fontFamily הוא Rubik, כל Text ישתמש בפונט הזה. זה מבטיח עקביות בכל האפליקציה ומונע צורך להגדיר את אותם ערכים שוב ושוב.

  5. visibleFrom לעומת hiddenFrom - visibleFrom="sm" מציג את האלמנט רק כשהמסך הוא sm ומעלה (מוסתר בטלפון). hiddenFrom="md" מסתיר את האלמנט כשהמסך הוא md ומעלה (מוצג רק בטלפון וטאבלט). למעשה הם הפוכים - visibleFrom מגדיר מאיזה גודל להציג, hiddenFrom מגדיר מאיזה גודל להסתיר.