Next.js: التخطيطات والقوالب

آخر تحديث: 2026-08-26

نظام التخطيط يشبه طوابق المبنى — كل طابق (layout) يحتوي على ممرات ومرافق مشتركة، بينما يمكن لكل غرفة (page) أن يكون لها ديكور مختلف، ويتم الحفاظ على حالة مستخدمي المساحة أثناء تنقلهم بين الطوابق.

1. ما ستتعلمه



2. قصة حقيقية لمطور Full-Stack

(1) نقطة الألم: الاضطرار إلى كتابة شريط التنقل والشريط الجانبي بشكل متكرر لكل صفحة

أثناء تطوير لوحة إدارة TaskFlow، واجهت Alice مشكلة تكرار كود التخطيط:

"فريقنا يضم خمسة مطورين، كل منهم يعمل على صفحات مختلفة. يضطر الجميع إلى استيراد شريط التنقل والشريط الجانبي يدويًا في ملفات page.tsx الخاصة بهم. الأسبوع الماضي، نسي Charlie إضافة الشريط الجانبي إلى صفحة settings/page.tsx المنشأة حديثًا، فعندما نقر المستخدم على صفحة الإعدادات، اختفت القائمة فجأة، واعتقد أن التنقل معطل."

تكرار الكود:

المشكلة التأثير عدد الصفحات المتأثرة
استيراد مكرر لشريط التنقل يجب تضمينه يدويًا في كل صفحة 15 صفحة
حالة الشريط الجانبي غير مستمرة الحالة المحددة في الشريط الجانبي تُفقد بعد التنقل جميع الصفحات
ظهور شريط التنقل في صفحة تسجيل الدخول/التسجيل لا ينبغي عرضه؛ يتطلب تحققًا شرطيًا إضافيًا 3 صفحات
نقل بيانات المستخدم المعقد يجب جلب بيانات المستخدم في كل صفحة 12 صفحة

(2) حلول نظام التخطيط في Next.js

استخدم التخطيطات المتداخلة ومجموعات المسارات لعزل التخطيطات؛ عرّفها مرة واحدة للتطبيق الشامل.

TEXT 📖 للعرض فقط
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) سلوك استمرارية التخطيط

100%
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 ❌ لا يعمل ✅ يعمل
جلب البيانات ❌ لا يُجلب ✅ يُجلب

▶ مثال: عرض توضيحي لاستمرارية التخطيط

المخرجات:

TEXT 📖 للعرض فقط
مخطط: التخطيط يستمر عبر التنقل (يحافظ على الحالة)، القالب يُعاد تحميله (نسخة جديدة).
TSX
// ============================================
// عرض توضيحي لاستمرارية التخطيط مقابل إعادة تحميل الصفحة
// ============================================

// 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>
  );
}

المخرجات:

TEXT 📖 للعرض فقط
مكون تفاعلي مع حالة: sidebarState.
TSX
// 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>
  );
}
TSX
// 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>
  );
}

المخرجات:

TEXT 📖 للعرض فقط
1. زر /dashboard، وسّع الشريط الجانبي، يظهر محتوى Dashboard
2. انقر "← طي"، يطوى الشريط الجانبي إلى 64px
3. انقر رابط "Projects"، انتقل إلى /dashboard/projects
4. يبقى الشريط الجانبي مطويًا(لم يُعاد تعيينه إلى الموسع)✅
5. محتوى الصفحة يتحول من Dashboard إلى Projects ✅

(2) التسلسل الهرمي للتخطيطات المتداخلة

100%
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>، الخطوط العامة، الأنماط العامة
تخطيط المجموعة الصفحات داخل المجموعة الشريط الجانبي، شريط التنقل، معلومات المستخدم
التخطيط المتداخل صفحات المجلدات الفرعية التنقل الفرعي، مسارات التنقل، شريط التصفية المحلي

▶ مثال: تخطيط متداخل بثلاثة مستويات

المخرجات:

TEXT 📖 للعرض فقط
مخطط لتسلسل التخطيط المتداخل من الجذر → التخطيط الأب → التخطيطات الفرعية.
TSX
// ============================================
// تخطيط متداخل بثلاثة مستويات: عام → الخلفية → إدارة المنتجات
// ============================================

// المستوى 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>
  );
}

المخرجات:

TEXT 📖 للعرض فقط
RootLayout يعرض واجهته.

المخرجات:

TEXT 📖 للعرض فقط
زر /dashboard/products:
→ تخطيط الجذر يعرض <html><body>
→ تخطيط الخلفية يعرض <Sidebar> + <main>
→ تخطيط المنتجات يعرض التنقل الفرعي للمنتجات + محتوى الصفحة
جميع المستويات الثلاثة للتداخل سارية المفعول


4. مجموعات المسارات (group)

(1) ما هي مجموعات المسارات؟

مجلدات (group) لا تولد مقاطع مسار في عناوين URL؛ تُستخدم فقط للتجميع المنطقي.

100%
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 المسار الفرعي طبيعي

▶ مثال: استخدام مجموعات المسارات لتنفيذ تخطيطات مختلفة

المخرجات:

TEXT 📖 للعرض فقط
مخطط لهيكل المسارات: مسارات نظام الملفات تُعيّن إلى مسارات URL.
TSX
// ============================================
// استخدام مجموعات المسارات لفصل تخطيطات صفحة تسجيل الدخول ولوحة الإدارة
// ============================================

// 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>
  );
}

المخرجات:

TEXT 📖 للعرض فقط
يعرض: TaskFlow | تعاون وأنجز
TSX
// 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>
  );
}

المخرجات:

TEXT 📖 للعرض فقط
زر /login لترى:
- تصميم نموذج مركزي(بدون شريط تنقل، بدون شريط جانبي)
- خلفية بتدرج أزرق
- شعار TaskFlow + نموذج تسجيل الدخول

زر /dashboard لترى:
- شريط جانبي داكن على اليسار(Dashboard / Projects / Settings)
- شريط بحث أبيض في الأعلى
- منطقة محتوى على اليمين


5. التخطيط مقابل القالب

(1) الفروق الرئيسية

100%
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

المخرجات:

TEXT 📖 للعرض فقط
مخطط: التخطيط يستمر عبر التنقل (يحافظ على الحالة)، القالب يُعاد تحميله (نسخة جديدة).
TSX
// ============================================
// عرض مقارنة سلوك 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>
  );
}

المخرجات:

TEXT 📖 للعرض فقط
مكون تفاعلي مع حالة: count.
TSX
// ============================================
// 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>
  );
}
TSX
// 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>;
}

المخرجات:

TEXT 📖 للعرض فقط
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 مستمرة

▶ مثال: تنفيذ حركات انتقال الصفحة باستخدام القوالب

المخرجات:

TEXT 📖 للعرض فقط
المكون يعرض واجهة المستخدم الموصوفة في المتصفح.
TSX
// ============================================
// 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>
  );
}

المخرجات:

TEXT 📖 للعرض فقط
يعرض واجهة مكون Template.

المخرجات:

TEXT 📖 للعرض فقط
في كل مرة تتنقل إلى صفحة:
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

▶ مثال: تخطيط جذر كامل

المخرجات:

TEXT 📖 للعرض فقط
تُعرض الصفحة كما هو موصوف أعلاه، مع تحديث الواجهة بناءً على السلوك الموصوف.
TSX
// ============================================
// تخطيط الجذر — تكوين كامل
// الملف: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>
  );
}

المخرجات:

TEXT 📖 للعرض فقط
يعرض: تخطيط الجذر مع خط Inter، فئات CSS العامة، وقالب SEO metadata ("%s | TaskFlow").

المخرجات:

TEXT 📖 للعرض فقط
جميع الصفحات تُولَّد تلقائيًا بـ:
- خط Inter (تحسين الأداء، صفر CLS)
- فئات CSS العامة (antialiased, bg-gray-50, text-gray-900)
- SEO metadata(قالب العنوان "Page | TaskFlow")
- وسوم Open Graph(بطاقات مشاركة وسائل التواصل الاجتماعي)


7. تصميم نماذج مشاركة البيانات

(1) ثلاثة نماذج للمشاركة

100%
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 الكامل

TSX
// ============================================
// مثال شامل: نظام تخطيط 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>
  );
}

المخرجات المتوقعة:

TEXT 📖 للعرض فقط
زر /login:
- خلفية شاشة كاملة بتدرج أزرق داكن
- بطاقة نموذج بيضاء مركزية
- شعار TaskFlow + نموذج تسجيل الدخول

زر /dashboard:
- شريط جانبي داكن على اليسار(قابل للطي، يحافظ على الحالة أثناء التنقل)
- شريط تنقل علوي(يعرض مرحبًا، Alice + أيقونة الإشعارات)
- منطقة محتوى على اليمين(نظرة عامة على Dashboard + 3 بطاقات إحصائية)
- بيانات المستخدم تُنقل عبر Context للمشاركة

❓ أسئلة شائعة

س هل يمكنني استخدام 'use client' في layout.tsx؟
ج نعم. layout.tsx هو Server Component افتراضيًا، ولكن إذا كنت بحاجة إلى استخدام useState أو useEffect أو معالجات الأحداث، يمكنك إضافة توجيه 'use client'. ملاحظة: بمجرد إضافته، يصبح هذا التخطيط وجميع مكوناته الفرعية Client Components.
س كيف تؤثر مجموعات المسارات على عناوين URL؟
ج لا تؤثر على الإطلاق. عنوان URL لـ (auth)/login/page.tsx يبقى /login؛ (auth) تؤثر فقط على هيكل التخطيط ولا تولد مقاطع مسار URL. هذا هو الغرض الأساسي من مجموعات المسارات.
س هل يمكن أن يتعايش layout و template معًا؟
ج نعم. إذا كان كل من layout.tsx و template.tsx موجودين في نفس المجلد، فسيتم تغليف template داخل layout. عندما يتنقل المستخدم، يبقى layout، ويُعاد بناء template.
س هل يمكن تعديل وسم <html> في تخطيط الجذر؟
ج نعم، ويجب عليك كتابته يدويًا. تخطيط الجذر يستبدل تلقائيًا غلاف HTML الافتراضي لـ Next.js، لذا يمكنك إضافة سمات بحرية مثل lang و dir و className وما إلى ذلك. هذا هو مفتاح دعم RTL.
س ما هو الحد الأقصى لعدد مستويات التخطيط المتداخل؟
ج لا يوجد حد صارم، لكننا نوصي بعدم تجاوز 3-4 مستويات. كل مستوى تخطيط ينشئ div تغليف إضافي في DOM، ويمكن أن تؤثر المستويات المتداخلة الكثيرة على الأداء وقابلية الصيانة.
س كيف يمكن مشاركة البيانات عبر مجموعات مسارات مختلفة؟
ج من خلال Context Provider في تخطيط الجذر. بتغليف UserProvider و ThemeProvider وما إلى ذلك في تخطيط الجذر، يمكن للصفحات داخل جميع مجموعات المسارات الوصول إليها. هذه هي طريقة المشاركة العامة للبيانات التي توصي بها Next.js.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): أنشئ تخطيطي مجموعتي مسارات، (marketing)/layout.tsx و (app)/layout.tsx، في المشروع، وطبق أنماطًا مختلفة على صفحات التسويق (Home، About) وصفحات التطبيق (Dashboard، Settings) على التوالي.

  2. تمرين متقدم (⭐⭐): ضع كلًا من layout.tsx و template.tsx في نفس المجلد. أضف useEffect(() => { console.log('mounted') }, []) إلى كل ملف، ثم تنقل عبر الصفحات ولاحظ مخرجات console للتحقق من الفروق في كيفية تحميلهما.

  3. تحدٍّ (⭐⭐⭐): أنشئ UserProvider (Context) في تخطيط الجذر، واستخدم useUser() في كل من (auth)/login/page.tsx و (dashboard)/page.tsx لقراءة بيانات المستخدم، للتحقق من أن Context يشارك البيانات عبر مجموعات المسارات.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%