לדלג לתוכן

7.4 ריאקט ראוטר פתרון

פתרון - ריאקט ראוטר - React Router


פתרון תרגיל 1 - אתר תדמית בסיסי

import {
  BrowserRouter,
  Routes,
  Route,
  NavLink,
  useNavigate,
} from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <Navigation />
      <main style={{ padding: "20px" }}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/services" element={<Services />} />
          <Route path="/contact" element={<Contact />} />
          <Route path="*" element={<NotFound />} />
        </Routes>
      </main>
    </BrowserRouter>
  );
}

function Navigation() {
  const linkStyle = ({ isActive }: { isActive: boolean }) => ({
    padding: "10px 20px",
    textDecoration: "none",
    color: isActive ? "white" : "#333",
    backgroundColor: isActive ? "#3b82f6" : "transparent",
    borderRadius: "4px",
  });

  return (
    <nav
      style={{
        display: "flex",
        gap: "8px",
        padding: "16px",
        backgroundColor: "#f5f5f5",
      }}
    >
      <NavLink to="/" style={linkStyle} end>בית</NavLink>
      <NavLink to="/about" style={linkStyle}>אודות</NavLink>
      <NavLink to="/services" style={linkStyle}>שירותים</NavLink>
      <NavLink to="/contact" style={linkStyle}>צרו קשר</NavLink>
    </nav>
  );
}

function Home() {
  return (
    <div>
      <h1>ברוכים הבאים</h1>
      <p>אתר התדמית שלנו</p>
    </div>
  );
}

function About() {
  return (
    <div>
      <h1>אודותינו</h1>
      <p>אנחנו חברת פיתוח תוכנה מובילה</p>
    </div>
  );
}

function Services() {
  return (
    <div>
      <h1>השירותים שלנו</h1>
      <ul>
        <li>פיתוח אתרים</li>
        <li>פיתוח אפליקציות</li>
        <li>עיצוב UI/UX</li>
      </ul>
    </div>
  );
}

function Contact() {
  return (
    <div>
      <h1>צרו קשר</h1>
      <p>אימייל: info@example.com</p>
      <p>טלפון: 050-1234567</p>
    </div>
  );
}

function NotFound() {
  const navigate = useNavigate();
  return (
    <div style={{ textAlign: "center" }}>
      <h1>404 - הדף לא נמצא</h1>
      <p>הדף שחיפשת לא קיים</p>
      <button onClick={() => navigate("/")}>חזרה לדף הבית</button>
    </div>
  );
}

הסבר:
- הפרופ end ב-NavLink של דף הבית מונע מהלינק להיות פעיל בכל נתיב (כי "/" מתאים לכל נתיב)
- הפונקציה linkStyle מקבלת אובייקט עם isActive ומחזירה סגנון מתאים
- דף 404 תופס כל נתיב שלא הוגדר עם path="*"


פתרון תרגיל 2 - בלוג עם נתיבים דינמיים

import {
  BrowserRouter,
  Routes,
  Route,
  Link,
  useParams,
  useNavigate,
} from "react-router-dom";

interface Post {
  id: number;
  title: string;
  date: string;
  excerpt: string;
  content: string;
}

const posts: Post[] = [
  {
    id: 1,
    title: "מבוא לריאקט",
    date: "2024-01-15",
    excerpt: "למדו את הבסיס של ריאקט",
    content: "ריאקט היא ספריית JavaScript לבניית ממשקי משתמש. היא פותחה על ידי פייסבוק ומאפשרת לבנות קומפוננטות UI ניתנות לשימוש חוזר.",
  },
  {
    id: 2,
    title: "הוקים מתקדמים",
    date: "2024-02-20",
    excerpt: "הוקים מתקדמים בריאקט",
    content: "הוקים מתקדמים כמו useMemo, useCallback ו-useRef מאפשרים אופטימיזציה של ביצועים ושליטה דקה יותר בקומפוננטות.",
  },
  {
    id: 3,
    title: "ניהול סטייט",
    date: "2024-03-10",
    excerpt: "איך לנהל state באפליקציות ריאקט",
    content: "ניהול state הוא אחד האתגרים המרכזיים בפיתוח אפליקציות ריאקט. ישנן מספר גישות: useState, useReducer, Context, וספריות חיצוניות.",
  },
];

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<PostList />} />
        <Route path="/posts/:postId" element={<PostDetail />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

function PostList() {
  return (
    <div>
      <h1>הבלוג שלנו</h1>
      {posts.map((post) => (
        <article
          key={post.id}
          style={{
            border: "1px solid #ddd",
            padding: "16px",
            margin: "12px 0",
            borderRadius: "8px",
          }}
        >
          <Link to={`/posts/${post.id}`} style={{ textDecoration: "none" }}>
            <h2>{post.title}</h2>
          </Link>
          <p style={{ color: "#666" }}>
            {new Date(post.date).toLocaleDateString("he-IL")}
          </p>
          <p>{post.excerpt}</p>
          <Link to={`/posts/${post.id}`}>קרא עוד</Link>
        </article>
      ))}
    </div>
  );
}

function PostDetail() {
  const { postId } = useParams<{ postId: string }>();
  const navigate = useNavigate();
  const post = posts.find((p) => p.id === Number(postId));

  if (!post) {
    return (
      <div>
        <h1>פוסט לא נמצא</h1>
        <p>הפוסט עם מספר {postId} לא קיים</p>
        <button onClick={() => navigate("/")}>חזרה לרשימה</button>
      </div>
    );
  }

  return (
    <article>
      <button onClick={() => navigate("/")} style={{ marginBottom: "16px" }}>
        חזרה לרשימה
      </button>
      <h1>{post.title}</h1>
      <p style={{ color: "#666" }}>
        {new Date(post.date).toLocaleDateString("he-IL")}
      </p>
      <div>{post.content}</div>
    </article>
  );
}

function NotFound() {
  const navigate = useNavigate();
  return (
    <div>
      <h1>404</h1>
      <button onClick={() => navigate("/")}>חזרה</button>
    </div>
  );
}

הסבר:
- ה-URL /posts/:postId תופס את ה-ID מה-URL
- useParams מחזיר את הפרמטר כ-string, לכן ממירים ל-number בהשוואה
- אם הפוסט לא נמצא, מציגים הודעת שגיאה מתאימה


פתרון תרגיל 3 - לוח בקרה עם נתיבים מקוננים

import {
  BrowserRouter,
  Routes,
  Route,
  NavLink,
  Outlet,
  useParams,
  useLocation,
  Link,
} from "react-router-dom";

const users = [
  { id: 1, name: "דני כהן", email: "dani@example.com", role: "מפתח" },
  { id: 2, name: "מיכל לוי", email: "michal@example.com", role: "מעצבת" },
  { id: 3, name: "יוסי אברהם", email: "yossi@example.com", role: "מנהל" },
];

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/dashboard" element={<DashboardLayout />}>
          <Route index element={<DashboardOverview />} />
          <Route path="users" element={<UsersLayout />}>
            <Route index element={<UsersList />} />
            <Route path=":userId" element={<UserDetail />} />
          </Route>
          <Route path="products" element={<Products />} />
          <Route path="settings" element={<Settings />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

function Breadcrumbs() {
  const location = useLocation();
  const pathnames = location.pathname.split("/").filter(Boolean);

  const labels: Record<string, string> = {
    dashboard: "לוח בקרה",
    users: "משתמשים",
    products: "מוצרים",
    settings: "הגדרות",
  };

  return (
    <nav style={{ padding: "8px 0", fontSize: "14px" }}>
      {pathnames.map((segment, index) => {
        const path = `/${pathnames.slice(0, index + 1).join("/")}`;
        const isLast = index === pathnames.length - 1;
        const label = labels[segment] || segment;

        return (
          <span key={path}>
            {index > 0 && " / "}
            {isLast ? (
              <strong>{label}</strong>
            ) : (
              <Link to={path}>{label}</Link>
            )}
          </span>
        );
      })}
    </nav>
  );
}

function DashboardLayout() {
  const sidebarLinks = [
    { to: "/dashboard", label: "סקירה", end: true },
    { to: "/dashboard/users", label: "משתמשים", end: false },
    { to: "/dashboard/products", label: "מוצרים", end: false },
    { to: "/dashboard/settings", label: "הגדרות", end: false },
  ];

  return (
    <div style={{ display: "flex", minHeight: "100vh" }}>
      <aside
        style={{
          width: "200px",
          backgroundColor: "#1e293b",
          padding: "20px",
        }}
      >
        <h2 style={{ color: "white" }}>לוח בקרה</h2>
        <nav style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
          {sidebarLinks.map((link) => (
            <NavLink
              key={link.to}
              to={link.to}
              end={link.end}
              style={({ isActive }) => ({
                color: isActive ? "#3b82f6" : "#94a3b8",
                textDecoration: "none",
                padding: "8px",
                borderRadius: "4px",
                backgroundColor: isActive ? "#1e3a5f" : "transparent",
              })}
            >
              {link.label}
            </NavLink>
          ))}
        </nav>
      </aside>

      <main style={{ flex: 1, padding: "20px" }}>
        <Breadcrumbs />
        <Outlet />
      </main>
    </div>
  );
}

function DashboardOverview() {
  return (
    <div>
      <h1>סקירה כללית</h1>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "16px" }}>
        <div style={{ padding: "20px", backgroundColor: "#e0f2fe", borderRadius: "8px" }}>
          <h3>{users.length}</h3>
          <p>משתמשים</p>
        </div>
        <div style={{ padding: "20px", backgroundColor: "#dcfce7", borderRadius: "8px" }}>
          <h3>15</h3>
          <p>מוצרים</p>
        </div>
        <div style={{ padding: "20px", backgroundColor: "#fef3c7", borderRadius: "8px" }}>
          <h3>42</h3>
          <p>הזמנות</p>
        </div>
      </div>
    </div>
  );
}

function UsersLayout() {
  return <Outlet />;
}

function UsersList() {
  return (
    <div>
      <h1>משתמשים</h1>
      <table style={{ width: "100%", borderCollapse: "collapse" }}>
        <thead>
          <tr>
            <th>שם</th>
            <th>אימייל</th>
            <th>תפקיד</th>
            <th>פעולות</th>
          </tr>
        </thead>
        <tbody>
          {users.map((user) => (
            <tr key={user.id} style={{ borderBottom: "1px solid #eee" }}>
              <td>{user.name}</td>
              <td>{user.email}</td>
              <td>{user.role}</td>
              <td>
                <Link to={`/dashboard/users/${user.id}`}>צפה</Link>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

function UserDetail() {
  const { userId } = useParams();
  const user = users.find((u) => u.id === Number(userId));

  if (!user) return <p>משתמש לא נמצא</p>;

  return (
    <div>
      <Link to="/dashboard/users">חזרה לרשימה</Link>
      <h1>{user.name}</h1>
      <p>אימייל: {user.email}</p>
      <p>תפקיד: {user.role}</p>
    </div>
  );
}

function Products() {
  return <h1>מוצרים</h1>;
}

function Settings() {
  return <h1>הגדרות</h1>;
}

הסבר:
- ה-layout הראשי מכיל sidebar קבוע ו-Outlet לתוכן המשתנה
- ה-Breadcrumbs מפרקים את ה-pathname ומציגים כל חלק כקישור
- הנתיבים מקוננים בשתי רמות: dashboard ובתוכו users עם פרטי משתמש


פתרון תרגיל 4 - חנות מקוונת עם סינון ב-URL

import {
  BrowserRouter,
  Routes,
  Route,
  useSearchParams,
  useNavigate,
  useParams,
  Link,
} from "react-router-dom";
import { useMemo } from "react";

interface Product {
  id: number;
  name: string;
  category: string;
  price: number;
}

const products: Product[] = [
  { id: 1, name: "טלפון חכם", category: "electronics", price: 2999 },
  { id: 2, name: "אוזניות", category: "electronics", price: 499 },
  { id: 3, name: "חולצה", category: "clothing", price: 149 },
  { id: 4, name: "מכנסיים", category: "clothing", price: 199 },
  { id: 5, name: "מחשב נייד", category: "electronics", price: 4999 },
  { id: 6, name: "שעון", category: "accessories", price: 899 },
];

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/products" element={<ProductListPage />} />
        <Route path="/products/:productId" element={<ProductDetailPage />} />
      </Routes>
    </BrowserRouter>
  );
}

function ProductListPage() {
  const [searchParams, setSearchParams] = useSearchParams();

  const category = searchParams.get("category") || "all";
  const sort = searchParams.get("sort") || "name";
  const minPrice = Number(searchParams.get("minPrice")) || 0;
  const maxPrice = Number(searchParams.get("maxPrice")) || Infinity;

  const updateFilter = (key: string, value: string) => {
    setSearchParams((prev) => {
      if (value === "" || value === "all" || value === "0") {
        prev.delete(key);
      } else {
        prev.set(key, value);
      }
      return prev;
    });
  };

  const clearFilters = () => {
    setSearchParams({});
  };

  const filtered = useMemo(() => {
    let result = products.filter((p) => {
      if (category !== "all" && p.category !== category) return false;
      if (p.price < minPrice) return false;
      if (maxPrice !== Infinity && p.price > maxPrice) return false;
      return true;
    });

    result.sort((a, b) => {
      if (sort === "price-asc") return a.price - b.price;
      if (sort === "price-desc") return b.price - a.price;
      return a.name.localeCompare(b.name);
    });

    return result;
  }, [category, sort, minPrice, maxPrice]);

  return (
    <div>
      <h1>מוצרים</h1>
      <div style={{ display: "flex", gap: "12px", marginBottom: "20px" }}>
        <select value={category} onChange={(e) => updateFilter("category", e.target.value)}>
          <option value="all">כל הקטגוריות</option>
          <option value="electronics">אלקטרוניקה</option>
          <option value="clothing">ביגוד</option>
          <option value="accessories">אביזרים</option>
        </select>

        <select value={sort} onChange={(e) => updateFilter("sort", e.target.value)}>
          <option value="name">מיין לפי שם</option>
          <option value="price-asc">מחיר - מהנמוך לגבוה</option>
          <option value="price-desc">מחיר - מהגבוה לנמוך</option>
        </select>

        <input
          type="number"
          placeholder="מחיר מינימלי"
          value={minPrice || ""}
          onChange={(e) => updateFilter("minPrice", e.target.value)}
        />
        <input
          type="number"
          placeholder="מחיר מקסימלי"
          value={maxPrice === Infinity ? "" : maxPrice}
          onChange={(e) => updateFilter("maxPrice", e.target.value)}
        />

        <button onClick={clearFilters}>נקה פילטרים</button>
      </div>

      <p>מציג {filtered.length} מוצרים</p>

      <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "16px" }}>
        {filtered.map((product) => (
          <div key={product.id} style={{ border: "1px solid #ddd", padding: "16px" }}>
            <h3>{product.name}</h3>
            <p>{product.price} ש"ח</p>
            <Link to={`/products/${product.id}?${searchParams.toString()}`}>
              פרטים
            </Link>
          </div>
        ))}
      </div>
    </div>
  );
}

function ProductDetailPage() {
  const { productId } = useParams();
  const [searchParams] = useSearchParams();
  const navigate = useNavigate();
  const product = products.find((p) => p.id === Number(productId));

  if (!product) return <p>מוצר לא נמצא</p>;

  return (
    <div>
      <button onClick={() => navigate(`/products?${searchParams.toString()}`)}>
        חזרה למוצרים
      </button>
      <h1>{product.name}</h1>
      <p>קטגוריה: {product.category}</p>
      <p>מחיר: {product.price} ש"ח</p>
    </div>
  );
}

הסبر:
- כל הפילטרים נשמרים ב-URL כ-query parameters
- בניקוי פילטר (ערך ריק), המפתח נמחק מה-URL
- כשעוברים לדף מוצר, שומרים את ה-search params בלינק כדי שהכפתור "חזרה" ישמור את הפילטרים


פתרון תרגיל 5 - אותנטיקציה עם נתיבים מוגנים

import {
  BrowserRouter,
  Routes,
  Route,
  Navigate,
  Outlet,
  useNavigate,
  useLocation,
  NavLink,
} from "react-router-dom";
import { createContext, useContext, useState } from "react";

interface AuthContextType {
  user: { name: string; email: string } | null;
  login: (email: string, password: string) => boolean;
  logout: () => void;
}

const AuthContext = createContext<AuthContextType | null>(null);

function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error("useAuth must be used within AuthProvider");
  return ctx;
}

function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<{ name: string; email: string } | null>(null);

  const login = (email: string, password: string) => {
    if (password.length >= 4) {
      setUser({ name: "משתמש", email });
      return true;
    }
    return false;
  };

  const logout = () => setUser(null);

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

function ProtectedRoute() {
  const { user } = useAuth();
  const location = useLocation();

  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return <Outlet />;
}

function GuestRoute() {
  const { user } = useAuth();
  if (user) {
    return <Navigate to="/" replace />;
  }
  return <Outlet />;
}

function App() {
  return (
    <BrowserRouter>
      <AuthProvider>
        <Routes>
          <Route path="/" element={<Layout />}>
            <Route index element={<Home />} />

            <Route element={<GuestRoute />}>
              <Route path="login" element={<LoginPage />} />
              <Route path="register" element={<RegisterPage />} />
            </Route>

            <Route element={<ProtectedRoute />}>
              <Route path="profile" element={<ProfilePage />} />
              <Route path="settings" element={<SettingsPage />} />
              <Route path="dashboard" element={<DashboardPage />} />
            </Route>
          </Route>
        </Routes>
      </AuthProvider>
    </BrowserRouter>
  );
}

function Layout() {
  const { user, logout } = useAuth();
  const navigate = useNavigate();

  return (
    <div>
      <nav style={{ display: "flex", gap: "12px", padding: "16px", backgroundColor: "#f5f5f5" }}>
        <NavLink to="/">בית</NavLink>
        {user ? (
          <>
            <NavLink to="/profile">פרופיל</NavLink>
            <NavLink to="/dashboard">לוח בקרה</NavLink>
            <NavLink to="/settings">הגדרות</NavLink>
            <button onClick={() => { logout(); navigate("/"); }}>התנתק</button>
          </>
        ) : (
          <>
            <NavLink to="/login">התחבר</NavLink>
            <NavLink to="/register">הירשם</NavLink>
          </>
        )}
      </nav>
      <main style={{ padding: "20px" }}>
        <Outlet />
      </main>
    </div>
  );
}

function Home() {
  return <h1>דף הבית</h1>;
}

function LoginPage() {
  const { login } = useAuth();
  const navigate = useNavigate();
  const location = useLocation();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");

  const from = (location.state as any)?.from?.pathname || "/";

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (login(email, password)) {
      navigate(from, { replace: true });
    } else {
      setError("אימייל או סיסמה שגויים");
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <h1>התחברות</h1>
      {from !== "/" && <p>עליך להתחבר כדי לגשת ל-{from}</p>}
      {error && <p style={{ color: "red" }}>{error}</p>}
      <input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="אימייל" />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="סיסמה" />
      <button type="submit">התחבר</button>
    </form>
  );
}

function RegisterPage() {
  return <h1>הרשמה</h1>;
}

function ProfilePage() {
  const { user } = useAuth();
  return (
    <div>
      <h1>פרופיל</h1>
      <p>שם: {user?.name}</p>
      <p>אימייל: {user?.email}</p>
    </div>
  );
}

function SettingsPage() {
  return <h1>הגדרות</h1>;
}

function DashboardPage() {
  return <h1>לוח בקרה</h1>;
}

הסבר:
- GuestRoute מפנה משתמשים מחוברים לדף הבית (אין טעם שיראו דף התחברות)
- ProtectedRoute שומר את המיקום הנוכחי ב-state של Navigate
- בדף התחברות, קוראים את from מ-state ומפנים אליו אחרי התחברות מוצלחת


פתרון תרגיל 6 - אפליקציית מתכונים מלאה

הפתרון מורכב מהרבה חלקים, נציג את המבנה העיקרי:

import {
  BrowserRouter,
  Routes,
  Route,
  NavLink,
  Outlet,
  useParams,
  useSearchParams,
  useNavigate,
  Navigate,
  Link,
  useLocation,
} from "react-router-dom";
import { useState, useMemo, createContext, useContext } from "react";

interface Recipe {
  id: number;
  title: string;
  category: string;
  description: string;
  ingredients: string[];
  time: number;
}

const recipes: Recipe[] = [
  { id: 1, title: "פסטה ברוטב עגבניות", category: "איטלקי", description: "פסטה קלאסית", ingredients: ["פסטה", "עגבניות", "שום"], time: 30 },
  { id: 2, title: "חומוס ביתי", category: "ישראלי", description: "חומוס אמיתי", ingredients: ["חומוס", "טחינה", "לימון"], time: 60 },
  { id: 3, title: "סושי", category: "יפני", description: "סושי טרי", ingredients: ["אורז", "דג", "נורי"], time: 45 },
  { id: 4, title: "שקשוקה", category: "ישראלי", description: "שקשוקה חריפה", ingredients: ["עגבניות", "ביצים", "פלפל"], time: 25 },
];

// קונטקסט מועדפים
const FavoritesContext = createContext<{
  favorites: number[];
  toggleFavorite: (id: number) => void;
  isFavorite: (id: number) => boolean;
} | null>(null);

function useFavorites() {
  const ctx = useContext(FavoritesContext);
  if (!ctx) throw new Error("useFavorites requires FavoritesProvider");
  return ctx;
}

function FavoritesProvider({ children }: { children: React.ReactNode }) {
  const [favorites, setFavorites] = useState<number[]>(() => {
    const saved = localStorage.getItem("favorites");
    return saved ? JSON.parse(saved) : [];
  });

  const toggleFavorite = (id: number) => {
    setFavorites((prev) => {
      const next = prev.includes(id)
        ? prev.filter((f) => f !== id)
        : [...prev, id];
      localStorage.setItem("favorites", JSON.stringify(next));
      return next;
    });
  };

  const isFavorite = (id: number) => favorites.includes(id);

  return (
    <FavoritesContext.Provider value={{ favorites, toggleFavorite, isFavorite }}>
      {children}
    </FavoritesContext.Provider>
  );
}

function App() {
  return (
    <BrowserRouter>
      <FavoritesProvider>
        <Routes>
          <Route path="/" element={<AppLayout />}>
            <Route index element={<HomePage />} />
            <Route path="recipes" element={<RecipesPage />} />
            <Route path="recipes/:id" element={<RecipeDetailPage />} />
            <Route path="categories/:category" element={<CategoryPage />} />
            <Route path="favorites" element={<FavoritesPage />} />
            <Route path="*" element={<NotFoundPage />} />
          </Route>
        </Routes>
      </FavoritesProvider>
    </BrowserRouter>
  );
}

function Breadcrumbs() {
  const location = useLocation();
  const parts = location.pathname.split("/").filter(Boolean);

  const labels: Record<string, string> = {
    recipes: "מתכונים",
    categories: "קטגוריות",
    favorites: "מועדפים",
  };

  return (
    <nav style={{ padding: "8px 0", fontSize: "14px" }}>
      <Link to="/">בית</Link>
      {parts.map((part, i) => {
        const path = `/${parts.slice(0, i + 1).join("/")}`;
        const label = labels[part] || decodeURIComponent(part);
        const isLast = i === parts.length - 1;
        return (
          <span key={path}>
            {" / "}
            {isLast ? <strong>{label}</strong> : <Link to={path}>{label}</Link>}
          </span>
        );
      })}
    </nav>
  );
}

function AppLayout() {
  return (
    <div>
      <nav style={{ display: "flex", gap: "16px", padding: "16px", backgroundColor: "#f8f8f8" }}>
        <NavLink to="/">בית</NavLink>
        <NavLink to="/recipes">מתכונים</NavLink>
        <NavLink to="/favorites">מועדפים</NavLink>
      </nav>
      <main style={{ padding: "20px" }}>
        <Breadcrumbs />
        <Outlet />
      </main>
    </div>
  );
}

function RecipesPage() {
  const [searchParams, setSearchParams] = useSearchParams();
  const search = searchParams.get("search") || "";
  const category = searchParams.get("category") || "all";

  const filtered = useMemo(() => {
    return recipes.filter((r) => {
      if (category !== "all" && r.category !== category) return false;
      if (search && !r.title.includes(search)) return false;
      return true;
    });
  }, [search, category]);

  return (
    <div>
      <h1>מתכונים</h1>
      <input
        value={search}
        onChange={(e) => {
          setSearchParams((prev) => {
            if (e.target.value) prev.set("search", e.target.value);
            else prev.delete("search");
            return prev;
          });
        }}
        placeholder="חפש מתכון..."
      />
      <div>
        {filtered.map((recipe) => (
          <RecipeCard key={recipe.id} recipe={recipe} />
        ))}
      </div>
    </div>
  );
}

function RecipeCard({ recipe }: { recipe: Recipe }) {
  const { toggleFavorite, isFavorite } = useFavorites();

  return (
    <div style={{ border: "1px solid #ddd", padding: "16px", margin: "8px 0" }}>
      <Link to={`/recipes/${recipe.id}`}><h3>{recipe.title}</h3></Link>
      <p>{recipe.description}</p>
      <p>זמן הכנה: {recipe.time} דקות</p>
      <Link to={`/categories/${recipe.category}`}>{recipe.category}</Link>
      <button onClick={() => toggleFavorite(recipe.id)}>
        {isFavorite(recipe.id) ? "הסר ממועדפים" : "הוסף למועדפים"}
      </button>
    </div>
  );
}

function RecipeDetailPage() {
  const { id } = useParams();
  const navigate = useNavigate();
  const recipe = recipes.find((r) => r.id === Number(id));

  if (!recipe) {
    return (
      <div>
        <h1>מתכון לא נמצא</h1>
        <button onClick={() => navigate("/recipes")}>חזרה למתכונים</button>
      </div>
    );
  }

  return (
    <div>
      <button onClick={() => navigate(-1)}>חזרה</button>
      <h1>{recipe.title}</h1>
      <p>{recipe.description}</p>
      <p>זמן הכנה: {recipe.time} דקות</p>
      <h3>מצרכים:</h3>
      <ul>
        {recipe.ingredients.map((ing, i) => (
          <li key={i}>{ing}</li>
        ))}
      </ul>
    </div>
  );
}

function CategoryPage() {
  const { category } = useParams();
  const filtered = recipes.filter((r) => r.category === category);

  return (
    <div>
      <h1>קטגוריה: {category}</h1>
      {filtered.map((recipe) => (
        <RecipeCard key={recipe.id} recipe={recipe} />
      ))}
    </div>
  );
}

function FavoritesPage() {
  const { favorites } = useFavorites();
  const favoriteRecipes = recipes.filter((r) => favorites.includes(r.id));

  return (
    <div>
      <h1>המועדפים שלי</h1>
      {favoriteRecipes.length === 0 ? (
        <p>אין מועדפים עדיין. <Link to="/recipes">עבור למתכונים</Link></p>
      ) : (
        favoriteRecipes.map((recipe) => (
          <RecipeCard key={recipe.id} recipe={recipe} />
        ))
      )}
    </div>
  );
}

function HomePage() {
  return (
    <div>
      <h1>ברוכים הבאים לאפליקציית המתכונים</h1>
      <Link to="/recipes">צפו בכל המתכונים</Link>
    </div>
  );
}

function NotFoundPage() {
  return (
    <div style={{ textAlign: "center" }}>
      <h1>404 - דף לא נמצא</h1>
      <Link to="/">חזרה לדף הבית</Link>
    </div>
  );
}

הסبר:
- מועדפים מנוהלים בקונטקסט נפרד עם שמירה ב-localStorage
- חיפוש וסינון נשמרים ב-URL דרך useSearchParams
- Breadcrumbs מייצרים ניווט דינמי לפי ה-pathname הנוכחי
- כל דף מתכון ודף קטגוריה מטפלים במקרה שהנתון לא נמצא


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

  1. Link מול NavLink: Link הוא קישור בסיסי שמונע refresh. NavLink מוסיף מודעות למצב פעיל - יודע אם הנתיב שלו תואם ל-URL הנוכחי. נשתמש ב-NavLink בתפריטי ניווט שרוצים להדגיש את הדף הנוכחי, וב-Link בקישורים רגילים.

  2. navigate() מול Navigate: navigate() (מ-useNavigate) הוא פונקציה שנקראת מתוך event handlers או logic. Navigate הוא קומפוננטה שמבצעת ניווט כחלק מהרנדר (declarative). נשתמש ב-navigate() ב-onClick, handleSubmit וכדומה, וב-Navigate בתוך JSX לניתובים מותנים (כמו ProtectedRoute).

  3. useSearchParams מול useState: יתרונות URL: אפשר לשתף את הלינק עם הפילטרים, refresh שומר את המצב, כפתורי חזרה/קדימה בדפדפן עובדים, ו-SEO טוב יותר. חסרונות: מוגבל לנתונים שניתן לייצג כ-string ב-URL, ופחות מתאים למצבים מורכבים.

  4. איך Outlet עובד: Outlet הוא placeholder שבו ריאקט ראוטר מרנדר את הנתיב הילד המתאים. ברמות מקוננות, כל Outlet מרנדר את הרמה הבאה. לדוגמה: Layout Outlet מרנדר Dashboard, ו-Dashboard Outlet מרנדר Analytics. זה מאפשר layouts משותפים בכל רמה.

  5. replace: true מול false: בברירת מחדל (false), הניווט מוסיף כניסה חדשה להיסטוריה - המשתמש יכול ללחוץ "חזרה" ולחזור. עם replace: true, הנתיב הנוכחי מוחלף ואין אפשרות לחזור אליו. שימושי אחרי התחברות (אין טעם לחזור לדף login), אחרי redirect, ובניווטים שלא צריכים להישמר בהיסטוריה.