Next.js: التخطيطات والقوالب
آخر تحديث: 2026-08-26
نظام التخطيط يشبه طوابق المبنى — كل طابق (layout) يحتوي على ممرات ومرافق مشتركة، بينما يمكن لكل غرفة (page) أن يكون لها ديكور مختلف، ويتم الحفاظ على حالة مستخدمي المساحة أثناء تنقلهم بين الطوابق.
1. ما ستتعلمه
- سلوك الاستمرارية وطرق التكوين للتخطيطات المتداخلة
- تأثير التجميع المنطقي وعناوين URL في مجموعات المسارات
(group) - الفروق الرئيسية بين التخطيطات والقوالب ومتى تستخدم كلًا منهما
- التكوينات الأساسية لتخطيط الجذر:
<html>و<body>والخط و metadata - ثلاثة نماذج لمشاركة البيانات بين التخطيطات
2. قصة حقيقية لمطور Full-Stack
(1) نقطة الألم: الاضطرار إلى كتابة شريط التنقل والشريط الجانبي بشكل متكرر لكل صفحة
أثناء تطوير لوحة إدارة TaskFlow، واجهت Alice مشكلة تكرار كود التخطيط:
"فريقنا يضم خمسة مطورين، كل منهم يعمل على صفحات مختلفة. يضطر الجميع إلى استيراد شريط التنقل والشريط الجانبي يدويًا في ملفات page.tsx الخاصة بهم. الأسبوع الماضي، نسي Charlie إضافة الشريط الجانبي إلى صفحة
settings/page.tsxالمنشأة حديثًا، فعندما نقر المستخدم على صفحة الإعدادات، اختفت القائمة فجأة، واعتقد أن التنقل معطل."
تكرار الكود:
| المشكلة | التأثير | عدد الصفحات المتأثرة |
|---|---|---|
| استيراد مكرر لشريط التنقل | يجب تضمينه يدويًا في كل صفحة | 15 صفحة |
| حالة الشريط الجانبي غير مستمرة | الحالة المحددة في الشريط الجانبي تُفقد بعد التنقل | جميع الصفحات |
| ظهور شريط التنقل في صفحة تسجيل الدخول/التسجيل | لا ينبغي عرضه؛ يتطلب تحققًا شرطيًا إضافيًا | 3 صفحات |
| نقل بيانات المستخدم المعقد | يجب جلب بيانات المستخدم في كل صفحة | 12 صفحة |
(2) حلول نظام التخطيط في Next.js
استخدم التخطيطات المتداخلة ومجموعات المسارات لعزل التخطيطات؛ عرّفها مرة واحدة للتطبيق الشامل.
src/app/
├── layout.tsx # تخطيط الجذر(html/body/الخط)
├── page.tsx # الصفحة الرئيسية
├── (auth)/
│ ├── layout.tsx # تخطيط تسجيل الدخول/التسجيل(بدون شريط جانبي)
│ ├── login/page.tsx
│ └── register/page.tsx
└── (dashboard)/
├── layout.tsx # تخطيط لوحة الإدارة(تنقل+شريط جانبي)
├── page.tsx # لوحة التحكم
├── projects/page.tsx # قائمة المشاريع
└── settings/page.tsx # صفحة الإعدادات
(3) العائد
| البُعد | قبل (استيراد يدوي) | بعد (نظام التخطيط) |
|---|---|---|
| مراجع شريط التنقل | 15 سطر import عبر 15 صفحة |
ملف layout.tsx واحد |
| حالة تحديد الشريط الجانبي | تُفقد | مستمرة |
| تخطيط تسجيل الدخول/التسجيل | تحقق شرطي | عزل طبيعي عبر مجموعات المسارات |
| جلب بيانات المستخدم | 12 عملية جلب | تخطيط مشترك واحد |
3. مبادئ التخطيطات المتداخلة
(1) سلوك استمرارية التخطيط
graph TB
subgraph "تنقل الصفحة"
A[تخطيط الجذر] --> B[تخطيط dashboard]
B --> C[صفحة Dashboard]
B --> D[صفحة قائمة المشاريع]
B --> E[صفحة الإعدادات]
end
subgraph "السلوك أثناء التنقل"
F[layout يبقى محمّلًا<br/>لا فقدان للحالة]
G[page إلغاء تحميل/إعادة تحميل<br/>استبدال المحتوى]
end
style F fill:#d4edda
style G fill:#f8d7da
| السلوك | layout | page |
|---|---|---|
| إعادة التحميل أثناء التنقل | ❌ لا يُعاد تحميله | ✅ يُعاد تحميله |
| الحفاظ على حالة React | ✅ محفوظة | ❌ تُعاد تعيينها |
| إعادة تشغيل useEffect | ❌ لا يعمل | ✅ يعمل |
| جلب البيانات | ❌ لا يُجلب | ✅ يُجلب |
▶ مثال: عرض توضيحي لاستمرارية التخطيط
المخرجات:
مخطط: التخطيط يستمر عبر التنقل (يحافظ على الحالة)، القالب يُعاد تحميله (نسخة جديدة).
// ============================================
// عرض توضيحي لاستمرارية التخطيط مقابل إعادة تحميل الصفحة
// ============================================
// src/app/(dashboard)/layout.tsx — تخطيط الشريط الجانبي
'use client';
import { useState } from "react";
import Link from "next/link";
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const [sidebarState, setSidebarState] = useState("collapsed");
return (
<div className="flex h-screen">
{/* الشريط الجانبي — يحافظ على حالة التوسيع/الطي أثناء التنقل */}
<aside className={`bg-gray-800 text-white ${sidebarState === "collapsed" ? "w-16" : "w-64"} transition-all`}>
<button
onClick={() => setSidebarState(s =>
s === "collapsed" ? "expanded" : "collapsed"
)}
className="p-4 hover:bg-gray-700 w-full text-left"
>
{sidebarState === "collapsed" ? "→" : "← طي"}
</button>
<nav className="mt-4">
<Link href="/dashboard" className="block p-3 hover:bg-gray-700">Dashboard</Link>
<Link href="/dashboard/projects" className="block p-3 hover:bg-gray-700">Projects</Link>
<Link href="/dashboard/settings" className="block p-3 hover:bg-gray-700">Settings</Link>
</nav>
<div className="mt-4 p-3 text-sm text-gray-400">
حالة الشريط الجانبي تستمر عبر التنقل
</div>
</aside>
<main className="flex-1 p-8 overflow-auto">
{children}
</main>
</div>
);
}
المخرجات:
مكون تفاعلي مع حالة: sidebarState.
// src/app/(dashboard)/dashboard/page.tsx
export default function DashboardPage() {
return (
<div>
<h1 className="text-2xl font-bold">Dashboard</h1>
<p className="text-gray-600">مرحبًا بك في لوحة التحكم.</p>
</div>
);
}
// src/app/(dashboard)/dashboard/projects/page.tsx
export default function ProjectsPage() {
return (
<div>
<h1 className="text-2xl font-bold">Projects</h1>
<p className="text-gray-600">قائمة مشاريعك تظهر هنا.</p>
</div>
);
}
المخرجات:
1. زر /dashboard، وسّع الشريط الجانبي، يظهر محتوى Dashboard
2. انقر "← طي"، يطوى الشريط الجانبي إلى 64px
3. انقر رابط "Projects"، انتقل إلى /dashboard/projects
4. يبقى الشريط الجانبي مطويًا(لم يُعاد تعيينه إلى الموسع)✅
5. محتوى الصفحة يتحول من Dashboard إلى Projects ✅
(2) التسلسل الهرمي للتخطيطات المتداخلة
graph TB
A[تخطيط الجذر] --> B[app/layout.tsx]
B --> C[تخطيط (dashboard)]
C --> D[app/(dashboard)/layout.tsx]
D --> E[تخطيط products]
E --> F[app/(dashboard)/products/layout.tsx]
F --> G[page]
G --> H[app/(dashboard)/products/page.tsx]
style B fill:#cce5ff
style D fill:#d4edda
style F fill:#f8d7da
| مستوى التخطيط | النطاق | المحتوى المشترك |
|---|---|---|
| تخطيط الجذر | جميع الصفحات | <html>، <body>، الخطوط العامة، الأنماط العامة |
| تخطيط المجموعة | الصفحات داخل المجموعة | الشريط الجانبي، شريط التنقل، معلومات المستخدم |
| التخطيط المتداخل | صفحات المجلدات الفرعية | التنقل الفرعي، مسارات التنقل، شريط التصفية المحلي |
▶ مثال: تخطيط متداخل بثلاثة مستويات
المخرجات:
مخطط لتسلسل التخطيط المتداخل من الجذر → التخطيط الأب → التخطيطات الفرعية.
// ============================================
// تخطيط متداخل بثلاثة مستويات: عام → الخلفية → إدارة المنتجات
// ============================================
// المستوى 1: src/app/layout.tsx — تخطيط الجذر
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className="bg-gray-50">
{children}
</body>
</html>
);
}
// المستوى 2: src/app/(dashboard)/layout.tsx — تخطيط الخلفية
export default function DashboardLayout({ children }) {
return (
<div className="flex">
<Sidebar />
<main className="flex-1">{children}</main>
</div>
);
}
// المستوى 3: src/app/(dashboard)/products/layout.tsx — مجموعة المنتجات
export default function ProductsLayout({ children }) {
return (
<div>
<nav className="flex gap-4 border-b pb-2 mb-4">
<a href="/products" className="text-blue-600">جميع المنتجات</a>
<a href="/products/add" className="text-blue-600">إضافة منتج</a>
<a href="/products/categories" className="text-blue-600">الفئات</a>
</nav>
{children}
</div>
);
}
المخرجات:
RootLayout يعرض واجهته.
المخرجات:
زر /dashboard/products:
→ تخطيط الجذر يعرض <html><body>
→ تخطيط الخلفية يعرض <Sidebar> + <main>
→ تخطيط المنتجات يعرض التنقل الفرعي للمنتجات + محتوى الصفحة
جميع المستويات الثلاثة للتداخل سارية المفعول
4. مجموعات المسارات (group)
(1) ما هي مجموعات المسارات؟
مجلدات (group) لا تولد مقاطع مسار في عناوين URL؛ تُستخدم فقط للتجميع المنطقي.
graph LR
subgraph "هيكل الملفات"
A[app] --> B[(auth)]
A --> C[(dashboard)]
B --> D[login/page.tsx]
B --> E[register/page.tsx]
C --> F[page.tsx]
C --> G[settings/page.tsx]
end
subgraph "URL المقابل"
H[/login]
I[/register]
J[/dashboard]
K[/dashboard/settings]
end
style B fill:#f8d7da
style C fill:#d4edda
| المجلد | مسار URL | الوصف |
|---|---|---|
app/(auth)/login/page.tsx |
/login |
(auth) لا يولد مقاطع مسار |
app/(auth)/register/page.tsx |
/register |
(auth) لا يولد مقاطع مسار |
app/(dashboard)/page.tsx |
/dashboard |
(dashboard) لا يولد مقاطع مسار |
app/(dashboard)/settings/page.tsx |
/dashboard/settings |
المسار الفرعي طبيعي |
▶ مثال: استخدام مجموعات المسارات لتنفيذ تخطيطات مختلفة
المخرجات:
مخطط لهيكل المسارات: مسارات نظام الملفات تُعيّن إلى مسارات URL.
// ============================================
// استخدام مجموعات المسارات لفصل تخطيطات صفحة تسجيل الدخول ولوحة الإدارة
// ============================================
// src/app/(auth)/layout.tsx — تخطيط تسجيل الدخول/التسجيل(بدون تنقل، بدون شريط جانبي)
export default function AuthLayout({ children }) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900">TaskFlow</h1>
<p className="text-gray-500">تعاون وأنجز</p>
</div>
<div className="bg-white p-8 rounded-xl shadow-sm">
{children}
</div>
</div>
</div>
);
}
// src/app/(auth)/login/page.tsx
export default function LoginPage() {
return (
<form className="space-y-4">
<h2 className="text-xl font-bold text-center">تسجيل الدخول</h2>
<input
type="email"
placeholder="البريد الإلكتروني"
className="w-full p-3 border rounded-lg"
/>
<input
type="password"
placeholder="كلمة المرور"
className="w-full p-3 border rounded-lg"
/>
<button
type="submit"
className="w-full p-3 bg-blue-600 text-white rounded-lg"
>
تسجيل الدخول
</button>
</form>
);
}
المخرجات:
يعرض: TaskFlow | تعاون وأنجز
// src/app/(dashboard)/layout.tsx — تخطيط لوحة الإدارة(شريط جانبي + تنقل علوي)
export default function DashboardLayout({ children }) {
return (
<div className="flex h-screen">
<aside className="w-64 bg-gray-900 text-white">
<div className="p-4 text-xl font-bold">TaskFlow</div>
<nav className="mt-4">
<a href="/dashboard" className="block p-3 hover:bg-gray-800">
Dashboard
</a>
<a href="/dashboard/projects" className="block p-3 hover:bg-gray-800">
Projects
</a>
<a href="/dashboard/settings" className="block p-3 hover:bg-gray-800">
Settings
</a>
</nav>
</aside>
<div className="flex-1 flex flex-col">
<header className="bg-white shadow-sm p-4">
<input
type="search"
placeholder="بحث..."
className="w-64 p-2 border rounded"
/>
</header>
<main className="flex-1 p-8 overflow-auto">{children}</main>
</div>
</div>
);
}
المخرجات:
زر /login لترى:
- تصميم نموذج مركزي(بدون شريط تنقل، بدون شريط جانبي)
- خلفية بتدرج أزرق
- شعار TaskFlow + نموذج تسجيل الدخول
زر /dashboard لترى:
- شريط جانبي داكن على اليسار(Dashboard / Projects / Settings)
- شريط بحث أبيض في الأعلى
- منطقة محتوى على اليمين
5. التخطيط مقابل القالب
(1) الفروق الرئيسية
graph LR
A[التنقل إلى صفحة جديدة] --> B{layout أم template؟}
B -->|layout| C[يبقى محمّلًا<br/>حالة مستمرة]
B -->|template| D[إلغاء تحميل وإعادة تحميل<br/>إعادة تحميل]
C --> E[تحديث المكونات الفرعية]
D --> F[المكون الفرعي + جميع حاويات التغليف أُعيد بناؤها]
style C fill:#d4edda
style D fill:#f8d7da
| الخصائص | layout.tsx |
template.tsx |
|---|---|---|
| إعادة التحميل أثناء التنقل | ❌ يبقى محمّلًا | ✅ إلغاء تحميل + إعادة تحميل |
| الحفاظ على حالة React | ✅ محفوظة | ❌ تُعاد تعيينها |
| إعادة تشغيل useEffect | ❌ لا يعمل | ✅ يعمل |
| حركات انتقال الصفحة | غير مناسب | ✅ مناسب |
| تحديث البيانات | ❌ لا يُحدّث | ✅ يُحدّث في كل تنقل |
| الأداء | أفضل | أسوأ قليلًا (تكلفة إعادة البناء) |
▶ مثال: مقارنة سلوك layout مقابل template
المخرجات:
مخطط: التخطيط يستمر عبر التنقل (يحافظ على الحالة)، القالب يُعاد تحميله (نسخة جديدة).
// ============================================
// عرض مقارنة سلوك layout مقابل template
// ============================================
// src/app/(dashboard)/layout.tsx — استخدام layout(يحافظ على الحالة أثناء التنقل)
'use client';
import { useEffect, useState } from "react";
import Link from "next/link";
export default function DashboardLayout({ children }) {
const [count, setCount] = useState(0);
const [mountTime] = useState(new Date().toLocaleTimeString());
useEffect(() => {
console.log("Layout mounted at:", new Date().toLocaleTimeString());
}, []);
return (
<div className="border-2 border-blue-500 p-4 rounded m-4">
<div className="text-sm text-blue-600 mb-2">
[LAYOUT] حُمّل في: {mountTime} | العدد: {count}
<button onClick={() => setCount(c => c + 1)} className="ml-2 px-2 bg-blue-100 rounded">
+1
</button>
</div>
<nav className="flex gap-4 mb-4">
<Link href="/dashboard/page-a" className="text-blue-600">Page A</Link>
<Link href="/dashboard/page-b" className="text-blue-600">Page B</Link>
</nav>
{children}
</div>
);
}
المخرجات:
مكون تفاعلي مع حالة: count.
// ============================================
// template.tsx — يُعاد بناؤه أثناء التنقل
// الملف:src/app/(dashboard)/template.tsx
// ============================================
'use client';
import { useEffect, useState } from "react";
import Link from "next/link";
export default function DashboardTemplate({ children }) {
const [count, setCount] = useState(0);
const [mountTime] = useState(new Date().toLocaleTimeString());
useEffect(() => {
console.log("Template mounted at:", new Date().toLocaleTimeString());
}, []);
return (
<div className="border-2 border-red-500 p-4 rounded m-4">
<div className="text-sm text-red-600 mb-2">
[TEMPLATE] حُمّل في: {mountTime} | العدد: {count}
<button onClick={() => setCount(c => c + 1)} className="ml-2 px-2 bg-red-100 rounded">
+1
</button>
</div>
<nav className="flex gap-4 mb-4">
<Link href="/dashboard/page-a" className="text-red-600">Page A</Link>
<Link href="/dashboard/page-b" className="text-red-600">Page B</Link>
</nav>
{children}
</div>
);
}
// src/app/(dashboard)/page-a.tsx
export default function PageA() {
return <div className="text-lg">محتوى Page A</div>;
}
// src/app/(dashboard)/page-b.tsx
export default function PageB() {
return <div className="text-lg">محتوى Page B</div>;
}
المخرجات:
1. زر /dashboard/page-a:
[LAYOUT] حُمّل في: 10:30:00 | العدد: 0
[TEMPLATE] حُمّل في: 10:30:00 | العدد: 0
محتوى Page A
2. انقر زر العدد +1 (عدادا الحاويتين يزدادان بشكل مستقل)
3. انقر رابط "Page B":
[LAYOUT] حُمّل في: 10:30:00 | العدد: 1 ← layout يحافظ، الحالة محفوظة
[TEMPLATE] حُمّل في: 10:30:05 | العدد: 0 ← template يُعاد بناؤه، الحالة تُعاد تعيينها
محتوى Page B
الخلاصة: layout يحافظ على التحميل والحالة، template يُعاد بناؤه في كل تنقل
(2) متى تستخدم القالب
| السيناريو | التوصية | السبب |
|---|---|---|
| حركات انتقال الصفحة | template | حركات دخول/خروج framer-motion |
| يجب تحديث البيانات في كل تنقل | template | useEffect يعاد تشغيله |
| تتبع تحليلات التنقل | template | تشغيل كود التتبع في كل تنقل |
| شريط التنقل/الشريط الجانبي | layout | الحفاظ على التحديد والتوسيع/الطي |
| سلة التسوق/المشغل | layout | حالة UI مستمرة |
▶ مثال: تنفيذ حركات انتقال الصفحة باستخدام القوالب
المخرجات:
المكون يعرض واجهة المستخدم الموصوفة في المتصفح.
// ============================================
// template + framer-motion لتنفيذ حركات انتقال الصفحة
// ============================================
'use client';
import { motion } from "framer-motion";
export default function Template({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
);
}
المخرجات:
يعرض واجهة مكون Template.
المخرجات:
في كل مرة تتنقل إلى صفحة:
1. الصفحة القديمة تتلاشى للأعلى(opacity: 1→0, y: 0→-20)
2. الصفحة الجديدة تتلاشى من الأسفل(opacity: 0→1, y: 20→0)
3. مدة الحركة 300ms
4. layout يبقى، لا يُعاد بناؤه، تنطبق الحركات فقط على منطقة المحتوى
6. التكوين الأساسي لتخطيطات الجذر
(1) مسؤوليات تخطيط الجذر
تخطيط الجذر هو ملف التخطيط الوحيد المطلوب؛ يعرّف الإطار لجميع الصفحات:
| خيار التكوين | الكود | الوصف |
|---|---|---|
وسم <html> |
<html lang="en"> |
سمات اللغة التي تؤثر على SEO |
وسم <body> |
<body className="..."> |
فئة CSS عامة |
| تحميل الخطوط | next/font/google |
خطوط محسّنة للأداء |
| Metadata | export const metadata |
بيانات SEO الوصفية العامة |
| الأنماط العامة | import './globals.css' |
توجيهات Tailwind |
▶ مثال: تخطيط جذر كامل
المخرجات:
تُعرض الصفحة كما هو موصوف أعلاه، مع تحديث الواجهة بناءً على السلوك الموصوف.
// ============================================
// تخطيط الجذر — تكوين كامل
// الملف:src/app/layout.tsx
// ============================================
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
// تحسين خط Google(تحميل مسبق تلقائي + CSS size-adjust)
const inter = Inter({
subsets: ["latin"],
display: "swap",
variable: "--font-inter",
});
// بيانات SEO الوصفية العامة
export const metadata: Metadata = {
title: {
template: "%s | TaskFlow",
default: "TaskFlow - إدارة المشاريع",
},
description: "منصة تعاونية لإدارة المشاريع",
openGraph: {
title: "TaskFlow",
description: "تعاون وأنجز المشاريع بشكل أسرع",
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={inter.variable}>
<body className="antialiased bg-gray-50 text-gray-900 min-h-screen">
{children}
</body>
</html>
);
}
المخرجات:
يعرض: تخطيط الجذر مع خط Inter، فئات CSS العامة، وقالب SEO metadata ("%s | TaskFlow").
المخرجات:
جميع الصفحات تُولَّد تلقائيًا بـ:
- خط Inter (تحسين الأداء، صفر CLS)
- فئات CSS العامة (antialiased, bg-gray-50, text-gray-900)
- SEO metadata(قالب العنوان "Page | TaskFlow")
- وسوم Open Graph(بطاقات مشاركة وسائل التواصل الاجتماعي)
7. تصميم نماذج مشاركة البيانات
(1) ثلاثة نماذج للمشاركة
graph TB
subgraph "نماذج مشاركة البيانات"
A[1. تمرير Props<br/>layout → page]
B[2. Context Provider<br/>حالة عامة]
C[3. جلب بيانات متوازي<br/>layout + page يجلبان بشكل منفصل]
end
style A fill:#d4edda
style B fill:#cce5ff
style C fill:#f8d7da
| النموذج | السيناريوهات المناسبة | المميزات | العيوب |
|---|---|---|---|
| تمرير Props | layout يجلب البيانات ويمررها إلى page | آمن من حيث الأنواع | يمرر لمستوى واحد فقط |
| Context Provider | معلومات المستخدم، السمات | متاح عالميًا | مطلوب لـ Client Component |
| جلب بيانات متوازي | بيانات مستقلة لـ layout و page | فصل الاهتمامات، تزامن | لا يمكن المشاركة مباشرة |
8. مثال كامل: نظام تخطيط TaskFlow الكامل
// ============================================
// مثال شامل: نظام تخطيط TaskFlow الكامل
// تخطيط الجذر + تخطيط المصادقة + تخطيط لوحة التحكم + مجموعة المنتجات
// ============================================
// src/app/layout.tsx — تخطيط الجذر
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"], display: "swap" });
export const metadata: Metadata = {
title: { template: "%s | TaskFlow", default: "TaskFlow" },
description: "منصة إدارة المشاريع",
};
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.className}>
<body className="bg-gray-50 antialiased">{children}</body>
</html>
);
}
// src/app/(auth)/layout.tsx — تخطيط المصادقة
export default function AuthLayout({ children }) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-600 to-indigo-700">
<div className="w-full max-w-md">
<div className="text-center text-white mb-8">
<h1 className="text-4xl font-bold">TaskFlow</h1>
<p className="text-blue-200 mt-2">تعاون وأنجز</p>
</div>
<div className="bg-white rounded-xl shadow-2xl p-8">
{children}
</div>
</div>
</div>
);
}
// src/app/(dashboard)/layout.tsx — تخطيط Dashboard
'use client';
import { createContext, useContext, useState } from "react";
import Link from "next/link";
const DashboardContext = createContext(null);
export function useDashboard() { return useContext(DashboardContext); }
export default function DashboardLayout({ children }) {
const [sidebarOpen, setSidebarOpen] = useState(true);
const user = { name: "Alice", role: "Admin", avatar: "/avatar.png" };
return (
<DashboardContext.Provider value={{ user, sidebarOpen, setSidebarOpen }}>
<div className="flex h-screen">
<aside className={`bg-gray-900 text-white ${sidebarOpen ? "w-64" : "w-16"} transition-all duration-300`}>
<div className="p-4 flex items-center gap-3">
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-sm">
{user.name[0]}
</div>
{sidebarOpen && <span className="font-bold">TaskFlow</span>}
</div>
<nav className="mt-4">
<Link href="/dashboard" className="flex items-center gap-3 p-3 hover:bg-gray-800">
<span>📊</span>
{sidebarOpen && <span>Dashboard</span>}
</Link>
<Link href="/dashboard/projects" className="flex items-center gap-3 p-3 hover:bg-gray-800">
<span>📁</span>
{sidebarOpen && <span>Projects</span>}
</Link>
<Link href="/dashboard/team" className="flex items-center gap-3 p-3 hover:bg-gray-800">
<span>👥</span>
{sidebarOpen && <span>Team</span>}
</Link>
<Link href="/dashboard/settings" className="flex items-center gap-3 p-3 hover:bg-gray-800">
<span>⚙️</span>
{sidebarOpen && <span>Settings</span>}
</Link>
</nav>
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
className="absolute bottom-4 left-4 text-gray-400 hover:text-white"
>
{sidebarOpen ? "◀" : "▶"}
</button>
</aside>
<div className="flex-1 flex flex-col">
<header className="bg-white shadow-sm px-8 py-4 flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-700">
مرحبًا، {user.name}
</h2>
<div className="flex items-center gap-4">
<button className="text-gray-500">🔔</button>
<div className="w-8 h-8 bg-gray-300 rounded-full" />
</div>
</header>
<main className="flex-1 p-8 overflow-auto">{children}</main>
</div>
</div>
</DashboardContext.Provider>
);
}
// src/app/(dashboard)/dashboard/page.tsx — الصفحة الرئيسية لـ Dashboard
'use client';
import { useDashboard } from "../layout";
export default function DashboardHomePage() {
const { user } = useDashboard();
return (
<div>
<h1 className="text-3xl font-bold">نظرة عامة على Dashboard</h1>
<p className="text-gray-500 mt-2">
مرحبًا بعودتك، {user.name}! إليك ملخص مشروعك.
</p>
<div className="grid grid-cols-3 gap-6 mt-8">
<div className="bg-white p-6 rounded-xl shadow-sm">
<div className="text-sm text-gray-500">المشاريع النشطة</div>
<div className="text-3xl font-bold mt-2">12</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm">
<div className="text-sm text-gray-500">المهام المعلقة</div>
<div className="text-3xl font-bold mt-2">48</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm">
<div className="text-sm text-gray-500">أعضاء الفريق</div>
<div className="text-3xl font-bold mt-2">8</div>
</div>
</div>
</div>
);
}
المخرجات المتوقعة:
زر /login:
- خلفية شاشة كاملة بتدرج أزرق داكن
- بطاقة نموذج بيضاء مركزية
- شعار TaskFlow + نموذج تسجيل الدخول
زر /dashboard:
- شريط جانبي داكن على اليسار(قابل للطي، يحافظ على الحالة أثناء التنقل)
- شريط تنقل علوي(يعرض مرحبًا، Alice + أيقونة الإشعارات)
- منطقة محتوى على اليمين(نظرة عامة على Dashboard + 3 بطاقات إحصائية)
- بيانات المستخدم تُنقل عبر Context للمشاركة
❓ أسئلة شائعة
(auth)/login/page.tsx يبقى /login؛ (auth) تؤثر فقط على هيكل التخطيط ولا تولد مقاطع مسار URL. هذا هو الغرض الأساسي من مجموعات المسارات.<html> في تخطيط الجذر؟lang و dir و className وما إلى ذلك. هذا هو مفتاح دعم RTL.div تغليف إضافي في DOM، ويمكن أن تؤثر المستويات المتداخلة الكثيرة على الأداء وقابلية الصيانة.UserProvider و ThemeProvider وما إلى ذلك في تخطيط الجذر، يمكن للصفحات داخل جميع مجموعات المسارات الوصول إليها. هذه هي طريقة المشاركة العامة للبيانات التي توصي بها Next.js.📖 ملخص
- التخطيطات المتداخلة تبقى محمّلة أثناء التنقل، وحالة React محفوظة
- مجموعات المسارات
(group)لا تولد مقاطع مسار URL؛ تُستخدم فقط للتجميع المنطقي وعزل التخطيط - layout هو الأنسب لواجهات UI المستمرة، بينما template هو الأنسب للحركات وتحديث البيانات
- تخطيط الجذر يعمل كإطار لجميع الصفحات ويجب أن يتضمن
<html>و<body>و metadata - Context Provider هو الطريقة الموصى بها لمشاركة البيانات بين التخطيطات
- حالة الاستخدام الأكثر شيوعًا لمجموعات المسارات: فصل تخطيطات صفحة تسجيل الدخول (بدون تنقل) ولوحة الإدارة (مع تنقل)
- التخطيط يستخدم Server Component افتراضيًا؛ أضف 'use client' عند الحاجة للتفاعل
📝 تمارين
-
تمرين أساسي (⭐): أنشئ تخطيطي مجموعتي مسارات،
(marketing)/layout.tsxو(app)/layout.tsx، في المشروع، وطبق أنماطًا مختلفة على صفحات التسويق (Home، About) وصفحات التطبيق (Dashboard، Settings) على التوالي. -
تمرين متقدم (⭐⭐): ضع كلًا من
layout.tsxوtemplate.tsxفي نفس المجلد. أضفuseEffect(() => { console.log('mounted') }, [])إلى كل ملف، ثم تنقل عبر الصفحات ولاحظ مخرجات console للتحقق من الفروق في كيفية تحميلهما. -
تحدٍّ (⭐⭐⭐): أنشئ UserProvider (Context) في تخطيط الجذر، واستخدم
useUser()في كل من(auth)/login/page.tsxو(dashboard)/page.tsxلقراءة بيانات المستخدم، للتحقق من أن Context يشارك البيانات عبر مجموعات المسارات.