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)
תשובות לשאלות¶
-
SimpleGrid לעומת Grid - SimpleGrid מחלק את המרחב שווה בשווה בין כל הילדים, בלי אפשרות לשנות את גודל כל עמודה בנפרד. Grid מאפשר שליטה מלאה עם span (מתוך 12 עמודות), offset, ו-order. נשתמש ב-SimpleGrid לרשתות אחידות (כרטיסים, תכונות) וב-Grid כשצריך פרופורציות שונות (סיידבר + תוכן).
-
System Props - אלו תכונות CSS שאפשר להעביר ישירות כ-props לכל קומפוננטה של Mantine (כמו p, m, bg, c, w, h). הם חוסכים את הצורך לכתוב style={{ padding: "16px" }} או ליצור CSS class רק בשביל margin. במקום זה כותבים פשוט
<Box p="md" mt="lg">. -
יתרון AppShell - AppShell מספק מבנה אפליקציה מלא עם header, sidebar, footer ותוכן שכבר מטפל ב: position fixed של ה-header, margin/padding נכון של התוכן, רספונסיביות של הסיידבר (המבורגר בטלפון), ו-spacing מתאים. לבנות את כל זה מאפס דורש הרבה CSS וחישובים.
-
createTheme - הוא מגדיר ברירות מחדל גלובליות שחלות על כל הקומפוננטות. כש-primaryColor הוא green, כל Button ללא prop color יהיה ירוק. כש-fontFamily הוא Rubik, כל Text ישתמש בפונט הזה. זה מבטיח עקביות בכל האפליקציה ומונע צורך להגדיר את אותם ערכים שוב ושוב.
-
visibleFrom לעומת hiddenFrom -
visibleFrom="sm"מציג את האלמנט רק כשהמסך הוא sm ומעלה (מוסתר בטלפון).hiddenFrom="md"מסתיר את האלמנט כשהמסך הוא md ומעלה (מוצג רק בטלפון וטאבלט). למעשה הם הפוכים - visibleFrom מגדיר מאיזה גודל להציג, hiddenFrom מגדיר מאיזה גודל להסתיר.