9.2 ראוטר, דפים ולייאאוטים פתרון
פתרון - ראוטר, דפים ולייאאוטים - Router, Pages, and Layouts¶
פתרון תרגיל 1 - מבנה אתר בסיסי¶
מבנה התיקיות:
// app/page.tsx
export default function HomePage() {
return (
<main className="p-8">
<h1 className="text-4xl font-bold">ברוכים הבאים לחנות</h1>
<p className="mt-4">גלו את המוצרים המובחרים שלנו</p>
</main>
);
}
// app/products/page.tsx
export default function ProductsPage() {
return (
<div>
<h1 className="text-3xl font-bold">כל המוצרים</h1>
<p className="mt-2">בחרו קטגוריה מהתפריט</p>
</div>
);
}
// app/products/shoes/page.tsx
export default function ShoesPage() {
return (
<div>
<h1 className="text-3xl font-bold">נעליים</h1>
<p className="mt-2">מגוון נעליים איכותיות</p>
</div>
);
}
// app/products/shirts/page.tsx
export default function ShirtsPage() {
return (
<div>
<h1 className="text-3xl font-bold">חולצות</h1>
<p className="mt-2">חולצות בכל הסגנונות</p>
</div>
);
}
// app/about/page.tsx
export default function AboutPage() {
return (
<main className="p-8">
<h1 className="text-3xl font-bold">אודות</h1>
<p className="mt-2">חנות מקוונת מובילה מאז 2024</p>
</main>
);
}
// app/contact/page.tsx
export default function ContactPage() {
return (
<main className="p-8">
<h1 className="text-3xl font-bold">יצירת קשר</h1>
<p className="mt-2">שלחו לנו הודעה: info@shop.com</p>
</main>
);
}
פתרון תרגיל 2 - לייאאוטים מקוננים¶
// app/layout.tsx
import type { Metadata } from "next";
import Link from "next/link";
import "./globals.css";
export const metadata: Metadata = {
title: "החנות שלנו",
description: "חנות מקוונת עם Next.js",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="he" dir="rtl">
<body className="min-h-screen flex flex-col">
<nav className="bg-gray-800 text-white p-4">
<div className="max-w-6xl mx-auto flex gap-6">
<Link href="/" className="hover:text-gray-300">בית</Link>
<Link href="/products" className="hover:text-gray-300">מוצרים</Link>
<Link href="/about" className="hover:text-gray-300">אודות</Link>
<Link href="/contact" className="hover:text-gray-300">צור קשר</Link>
</div>
</nav>
<div className="flex-1">{children}</div>
<footer className="bg-gray-200 p-4 text-center">
<p>כל הזכויות שמורות 2024</p>
</footer>
</body>
</html>
);
}
// app/products/layout.tsx
import Link from "next/link";
export default function ProductsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex">
<aside className="w-56 bg-gray-50 p-4 min-h-[calc(100vh-120px)] border-l">
<h2 className="font-bold text-lg mb-4">קטגוריות</h2>
<nav className="space-y-2">
<Link
href="/products"
className="block p-2 rounded hover:bg-gray-200"
>
כל המוצרים
</Link>
<Link
href="/products/shoes"
className="block p-2 rounded hover:bg-gray-200"
>
נעליים
</Link>
<Link
href="/products/shirts"
className="block p-2 rounded hover:bg-gray-200"
>
חולצות
</Link>
</nav>
</aside>
<main className="flex-1 p-6">{children}</main>
</div>
);
}
- הלייאאוט הראשי מציג navbar ו-footer בכל הדפים
- לייאאוט המוצרים מוסיף sidebar רק בדפי המוצרים
- כשעוברים בין קטגוריות, רק התוכן הראשי (children) משתנה
פתרון תרגיל 3 - קבצים מיוחדים¶
// app/products/loading.tsx
export default function ProductsLoading() {
return (
<div className="p-8">
<div className="animate-pulse space-y-4">
<div className="h-8 bg-gray-200 rounded w-1/3" />
<div className="h-4 bg-gray-200 rounded w-2/3" />
<div className="grid grid-cols-3 gap-4 mt-6">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="h-48 bg-gray-200 rounded" />
))}
</div>
</div>
</div>
);
}
// app/products/error.tsx
"use client";
export default function ProductsError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="p-8 text-center">
<div className="text-6xl mb-4">!</div>
<h2 className="text-2xl font-bold text-red-600 mb-2">
שגיאה בטעינת המוצרים
</h2>
<p className="text-gray-600 mb-4">{error.message}</p>
<button
onClick={reset}
className="px-6 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
נסה שוב
</button>
</div>
);
}
// app/not-found.tsx
import Link from "next/link";
export default function NotFound() {
return (
<div className="min-h-[60vh] flex flex-col items-center justify-center text-center p-8">
<h1 className="text-8xl font-bold text-gray-300">404</h1>
<h2 className="text-2xl font-bold mt-4">הדף לא נמצא</h2>
<p className="text-gray-600 mt-2">
הדף שחיפשת לא קיים או שהועבר למקום אחר
</p>
<Link
href="/"
className="mt-6 px-6 py-3 bg-blue-500 text-white rounded hover:bg-blue-600"
>
חזרה לדף הבית
</Link>
</div>
);
}
בדיקת loading עם השהייה:
// app/products/page.tsx
export default async function ProductsPage() {
// השהייה מלאכותית לבדיקת loading
await new Promise((resolve) => setTimeout(resolve, 2000));
return (
<div>
<h1 className="text-3xl font-bold">כל המוצרים</h1>
<p className="mt-2">המוצרים נטענו בהצלחה</p>
</div>
);
}
פתרון תרגיל 4 - קבוצות נתיבים¶
מבנה מעודכן:
app/
layout.tsx
page.tsx
(shop)/
layout.tsx
products/
page.tsx
shoes/
page.tsx
shirts/
page.tsx
(info)/
layout.tsx
about/
page.tsx
contact/
page.tsx
// app/(shop)/layout.tsx
import Link from "next/link";
export default function ShopLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex">
<aside className="w-56 bg-blue-50 p-4 min-h-[calc(100vh-120px)]">
<h2 className="font-bold text-lg mb-4 text-blue-800">חנות</h2>
<nav className="space-y-2">
<Link href="/products" className="block p-2 rounded hover:bg-blue-100">
כל המוצרים
</Link>
<Link href="/products/shoes" className="block p-2 rounded hover:bg-blue-100">
נעליים
</Link>
<Link href="/products/shirts" className="block p-2 rounded hover:bg-blue-100">
חולצות
</Link>
</nav>
</aside>
<main className="flex-1 p-6">{children}</main>
</div>
);
}
// app/(info)/layout.tsx
export default function InfoLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="max-w-3xl mx-auto p-8">
<div className="bg-green-50 p-6 rounded-lg">
{children}
</div>
</div>
);
}
- הנתיבים נשארים
/products,/aboutוכו' - שם הקבוצה לא מופיע - כל קבוצה מקבלת לייאאוט ועיצוב משלה
פתרון תרגיל 5 - ניווט ולינק פעיל¶
// app/components/Navbar.tsx
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
const links = [
{ href: "/", label: "בית" },
{ href: "/products", label: "מוצרים" },
{ href: "/about", label: "אודות" },
{ href: "/contact", label: "צור קשר" },
];
export default function Navbar() {
const pathname = usePathname();
const isActive = (href: string) => {
if (href === "/") {
return pathname === "/";
}
return pathname.startsWith(href);
};
return (
<nav className="bg-gray-800 text-white p-4">
<div className="max-w-6xl mx-auto flex gap-6">
{links.map((link) => (
<Link
key={link.href}
href={link.href}
className={
isActive(link.href)
? "text-yellow-400 font-bold border-b-2 border-yellow-400 pb-1"
: "hover:text-gray-300 pb-1"
}
>
{link.label}
</Link>
))}
</div>
</nav>
);
}
- הפונקציה
isActiveבודקת אם הנתיב מתחיל עם ה-href של הלינק - לנתיב
/בודקים שוויון מדויק כדי שלא יהיה תמיד פעיל - כשנמצאים ב-
/products/shoes, גם הלינק של "מוצרים" מסומן כפעיל
פתרון תרגיל 6 - ניווט פרוגרמטי וחיפוש¶
// app/search/page.tsx
"use client";
import { useRouter, useSearchParams, usePathname } from "next/navigation";
import { useState } from "react";
export default function SearchPage() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const query = searchParams.get("q") || "";
const [input, setInput] = useState(query);
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
if (input.trim()) {
const params = new URLSearchParams(searchParams.toString());
params.set("q", input.trim());
router.push(`${pathname}?${params.toString()}`);
}
};
const handleClear = () => {
setInput("");
router.push(pathname);
};
return (
<main className="p-8 max-w-2xl mx-auto">
<h1 className="text-3xl font-bold mb-6">חיפוש</h1>
<form onSubmit={handleSearch} className="flex gap-2 mb-6">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="חפש..."
className="flex-1 border rounded px-4 py-2"
/>
<button
type="submit"
className="px-6 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
חפש
</button>
{query && (
<button
type="button"
onClick={handleClear}
className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
>
נקה
</button>
)}
</form>
{query ? (
<div>
<p className="text-lg">
מציג תוצאות עבור: <strong>{query}</strong>
</p>
<div className="mt-4 space-y-3">
<div className="border p-4 rounded">תוצאה 1 עבור "{query}"</div>
<div className="border p-4 rounded">תוצאה 2 עבור "{query}"</div>
<div className="border p-4 rounded">תוצאה 3 עבור "{query}"</div>
</div>
</div>
) : (
<p className="text-gray-500">הכנס מילת חיפוש כדי להתחיל</p>
)}
</main>
);
}
- השתמשנו ב-
URLSearchParamsכדי לבנות את ה-query string router.pushמעדכן את ה-URL בלי לרענן את הדף- כפתור "נקה" מנווט לנתיב בלי פרמטרים
תשובות לשאלות¶
-
ההבדל בין layout.tsx ל-template.tsx: שניהם עוטפים דפים, אבל layout לא מתרנדר מחדש בניווט ושומר על state. לעומת זאת, template יוצר instance חדש בכל ניווט - מתאים לאנימציות כניסה או למצבים שצריכים איפוס.
-
למה error.tsx חייב להיות Client Component: כי הוא משתמש ב-hooks (מקבל error ו-reset כ-props) וצריך להגיב לאינטראקציות משתמש (לחיצה על "נסה שוב"). React Error Boundaries עובדים רק כ-Client Components.
-
יתרון Link על תגית a רגילה:
Linkמבצע client-side navigation - רק התוכן שהשתנה מתעדכן, בלי לטעון את כל הדף מחדש. בנוסף, הוא עושה prefetch אוטומטי לדפים שהקישור מצביע אליהם, מה שהופך את הניווט למהיר יותר. -
Route Groups ומתי נשתמש בהם: Route Groups הם תיקיות בסוגריים שלא משפיעות על הנתיב. נשתמש בהם כשרוצים ארגון לוגי של קבצים (למשל הפרדה בין חלק שיווקי לחלק אפליקטיבי), או כשרוצים לייאאוטים שונים לקבוצות דפים שונות.
-
ההבדל בין router.push ל-router.replace:
pushמוסיף ערך חדש ל-history - המשתמש יכול ללחוץ "חזרה" ולהגיע לדף הקודם.replaceמחליף את הערך הנוכחי - המשתמש לא יכול לחזור.replaceמתאים למצבים כמו redirect אחרי login.