إطار Next.js

بناء تطبيق Next.js كامل - الجزء الثاني

60 دقيقة الدرس 40 من 40

بناء تطبيق Next.js كامل - الجزء الثاني

مرحباً بك في الجزء الثاني من بناء TaskFlow! في هذا الدرس، سنقوم بتنفيذ الميزات المتقدمة، وإضافة التعاون في الوقت الفعلي، وتحسين الأداء، وكتابة الاختبارات، ونشر تطبيقنا في الإنتاج.

الخطوة 6: إدارة المهام مع السحب والإفلات

تثبيت مكتبة السحب والإفلات:
npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
components/tasks/TaskBoard.tsx:
'use client';

import { useMemo, useState } from 'react';
import {
  DndContext,
  DragEndEvent,
  DragOverlay,
  DragStartEvent,
  PointerSensor,
  useSensor,
  useSensors,
} from '@dnd-kit/core';
import { SortableContext, arrayMove } from '@dnd-kit/sortable';
import { TaskColumn } from './TaskColumn';
import { TaskCard } from './TaskCard';

const COLUMNS = [
  { id: 'TODO', title: 'للإنجاز' },
  { id: 'IN_PROGRESS', title: 'قيد التنفيذ' },
  { id: 'IN_REVIEW', title: 'قيد المراجعة' },
  { id: 'DONE', title: 'مكتمل' },
];

export function TaskBoard({ tasks, projectId }: Props) {
  const [activeTask, setActiveTask] = useState(null);

  const sensors = useSensors(
    useSensor(PointerSensor, {
      activationConstraint: {
        distance: 8,
      },
    })
  );

  const tasksByStatus = useMemo(() => {
    return COLUMNS.reduce((acc, column) => {
      acc[column.id] = tasks.filter((task) => task.status === column.id);
      return acc;
    }, {} as Record<string, Task[]>);
  }, [tasks]);

  const handleDragStart = (event: DragStartEvent) => {
    const task = tasks.find((t) => t.id === event.active.id);
    setActiveTask(task);
  };

  const handleDragEnd = async (event: DragEndEvent) => {
    const { active, over } = event;

    if (!over || active.id === over.id) {
      setActiveTask(null);
      return;
    }

    const activeTask = tasks.find((t) => t.id === active.id);
    const overColumn = over.id as TaskStatus;

    if (activeTask && activeTask.status !== overColumn) {
      // تحديث حالة المهمة
      await fetch(\`/api/tasks/${activeTask.id}\`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ status: overColumn }),
      });

      // تشغيل إعادة التحقق أو تحديث الحالة المحلية
    }

    setActiveTask(null);
  };

  return (
    <DndContext
      sensors={sensors}
      onDragStart={handleDragStart}
      onDragEnd={handleDragEnd}
    >
      <div className="grid grid-cols-4 gap-4 h-full">
        {COLUMNS.map((column) => (
          <TaskColumn
            key={column.id}
            column={column}
            tasks={tasksByStatus[column.id] || []}
          />
        ))}
      </div>

      <DragOverlay>
        {activeTask ? <TaskCard task={activeTask} isDragging /> : null}
      </DragOverlay>
    </DndContext>
  );
}
components/tasks/TaskColumn.tsx:
'use client';

import { useDroppable } from '@dnd-kit/core';
import { SortableContext } from '@dnd-kit/sortable';
import { TaskCard } from './TaskCard';

export function TaskColumn({ column, tasks }: Props) {
  const { setNodeRef, isOver } = useDroppable({
    id: column.id,
  });

  return (
    <div
      ref={setNodeRef}
      className={cn(
        'flex flex-col bg-gray-50 rounded-lg p-4',
        isOver && 'bg-blue-50'
      )}
    >
      <div className="flex items-center justify-between mb-4">
        <h3 className="font-semibold">{column.title}</h3>
        <span className="text-sm text-gray-500">{tasks.length}</span>
      </div>

      <SortableContext items={tasks.map((t) => t.id)}>
        <div className="space-y-2 flex-1 overflow-y-auto">
          {tasks.map((task) => (
            <TaskCard key={task.id} task={task} />
          ))}
        </div>
      </SortableContext>
    </div>
  );
}

الخطوة 7: التعاون في الوقت الفعلي مع Pusher

lib/pusher.ts:
import Pusher from 'pusher';
import PusherClient from 'pusher-js';

// مثيل Pusher من جانب الخادم
export const pusherServer = new Pusher({
  appId: process.env.PUSHER_APP_ID!,
  key: process.env.NEXT_PUBLIC_PUSHER_KEY!,
  secret: process.env.PUSHER_SECRET!,
  cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER!,
  useTLS: true,
});

// مثيل Pusher من جانب العميل
export const pusherClient = new PusherClient(
  process.env.NEXT_PUBLIC_PUSHER_KEY!,
  {
    cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER!,
  }
);
app/api/tasks/[id]/route.ts (مع التحديثات في الوقت الفعلي):
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { prisma } from '@/lib/db';
import { pusherServer } from '@/lib/pusher';

export async function PATCH(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const session = await auth();

  if (!session?.user) {
    return NextResponse.json({ error: 'غير مصرح' }, { status: 401 });
  }

  const body = await request.json();
  const { status, priority, assigneeId, title, description, dueDate } = body;

  const task = await prisma.task.update({
    where: { id: params.id },
    data: {
      ...(status && { status }),
      ...(priority && { priority }),
      ...(assigneeId !== undefined && { assigneeId }),
      ...(title && { title }),
      ...(description !== undefined && { description }),
      ...(dueDate !== undefined && { dueDate }),
    },
    include: {
      assignee: {
        select: {
          id: true,
          name: true,
          image: true,
        },
      },
    },
  });

  // تشغيل التحديث في الوقت الفعلي
  await pusherServer.trigger(
    \`project-${task.projectId}\`,
    'task-updated',
    {
      task,
      updatedBy: {
        id: session.user.id,
        name: session.user.name,
      },
    }
  );

  return NextResponse.json(task);
}
hooks/useRealtimeUpdates.ts:
import { useEffect } from 'react';
import { pusherClient } from '@/lib/pusher';
import { useRouter } from 'next/navigation';

export function useRealtimeUpdates(projectId: string) {
  const router = useRouter();

  useEffect(() => {
    const channel = pusherClient.subscribe(\`project-${projectId}\`);

    channel.bind('task-updated', (data: any) => {
      // إعادة التحقق من الصفحة الحالية
      router.refresh();

      // إظهار الإشعار
      showNotification({
        title: 'تم تحديث المهمة',
        message: \`${data.updatedBy.name} قام بتحديث مهمة\`,
      });
    });

    channel.bind('task-created', (data: any) => {
      router.refresh();
      showNotification({
        title: 'مهمة جديدة',
        message: \`${data.createdBy.name} أنشأ مهمة\`,
      });
    });

    channel.bind('comment-added', (data: any) => {
      router.refresh();
      showNotification({
        title: 'تعليق جديد',
        message: \`${data.author.name} علق على مهمة\`,
      });
    });

    return () => {
      channel.unbind_all();
      pusherClient.unsubscribe(\`project-${projectId}\`);
    };
  }, [projectId, router]);
}

الخطوة 8: تنفيذ الاختبارات

تثبيت تبعيات الاختبار:
npm install -D @testing-library/react @testing-library/jest-dom jest jest-environment-jsdom
npm install -D @playwright/test
jest.config.js:
const nextJest = require('next/jest');

const createJestConfig = nextJest({
  dir: './',
});

const customJestConfig = {
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  testEnvironment: 'jest-environment-jsdom',
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/$1',
  },
};

module.exports = createJestConfig(customJestConfig);
__tests__/components/TaskCard.test.tsx:
import { render, screen } from '@testing-library/react';
import { TaskCard } from '@/components/tasks/TaskCard';

describe('TaskCard', () => {
  const mockTask = {
    id: '1',
    title: 'مهمة اختبار',
    description: 'وصف الاختبار',
    status: 'TODO',
    priority: 'HIGH',
    dueDate: new Date('2024-12-31'),
    assignee: {
      id: 'user1',
      name: 'أحمد محمد',
      image: '/avatar.jpg',
    },
  };

  it('يعرض عنوان المهمة', () => {
    render(<TaskCard task={mockTask} />);
    expect(screen.getByText('مهمة اختبار')).toBeInTheDocument();
  });

  it('يعرض شارة الأولوية', () => {
    render(<TaskCard task={mockTask} />);
    expect(screen.getByText('HIGH')).toBeInTheDocument();
  });

  it('يظهر معلومات المعين', () => {
    render(<TaskCard task={mockTask} />);
    expect(screen.getByText('أحمد محمد')).toBeInTheDocument();
  });
});
e2e/project-creation.spec.ts (Playwright):
import { test, expect } from '@playwright/test';

test('يمكن للمستخدم إنشاء مشروع جديد', async ({ page }) => {
  // تسجيل الدخول
  await page.goto('http://localhost:3000/login');
  await page.fill('input[name="email"]', 'test@example.com');
  await page.fill('input[name="password"]', 'password123');
  await page.click('button[type="submit"]');

  // الانتقال إلى المشاريع
  await page.waitForURL('**/dashboard');
  await page.click('text=مشروع جديد');

  // ملء نموذج المشروع
  await page.fill('input[name="name"]', 'مشروع الاختبار');
  await page.fill('textarea[name="description"]', 'هذا مشروع اختبار');
  await page.click('button:has-text("إنشاء")');

  // التحقق من إنشاء المشروع
  await expect(page.locator('text=مشروع الاختبار')).toBeVisible();
});

الخطوة 9: تحسين الأداء

تنفيذ الترقيم للمهام:
// app/api/projects/[id]/tasks/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const { searchParams } = new URL(request.url);
  const page = parseInt(searchParams.get('page') || '1');
  const limit = parseInt(searchParams.get('limit') || '20');
  const skip = (page - 1) * limit;

  const [tasks, total] = await Promise.all([
    prisma.task.findMany({
      where: { projectId: params.id },
      skip,
      take: limit,
      include: {
        assignee: true,
      },
      orderBy: {
        createdAt: 'desc',
      },
    }),
    prisma.task.count({
      where: { projectId: params.id },
    }),
  ]);

  return NextResponse.json({
    tasks,
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit),
    },
  });
}
إضافة فهارس قاعدة البيانات (prisma/schema.prisma):
model Task {
  // ... الحقول

  @@index([projectId])
  @@index([assigneeId])
  @@index([status])
  @@index([createdAt])
}

model Project {
  // ... الحقول

  @@index([slug])
  @@index([archived])
}
تنفيذ التخزين المؤقت للبيانات التي يتم الوصول إليها بشكل متكرر:
import { unstable_cache } from 'next/cache';

export const getProject = unstable_cache(
  async (id: string) => {
    return await prisma.project.findUnique({
      where: { id },
      include: {
        members: {
          include: {
            user: true,
          },
        },
        _count: {
          select: {
            tasks: true,
          },
        },
      },
    });
  },
  ['project'],
  {
    revalidate: 60, // التخزين المؤقت لمدة 60 ثانية
    tags: [\`project-${id}\`],
  }
);

الخطوة 10: النشر في الإنتاج

متغيرات البيئة للإنتاج (.env.production):
# قاعدة البيانات
DATABASE_URL="postgresql://user:password@production-host:5432/taskflow"

# المصادقة
NEXTAUTH_URL="https://taskflow.example.com"
NEXTAUTH_SECRET="secure-production-secret"

# OAuth
GOOGLE_CLIENT_ID="production-google-client-id"
GOOGLE_CLIENT_SECRET="production-google-client-secret"

# Pusher
NEXT_PUBLIC_PUSHER_KEY="production-pusher-key"
PUSHER_SECRET="production-pusher-secret"
PUSHER_APP_ID="production-app-id"
NEXT_PUBLIC_PUSHER_CLUSTER="mt1"

# المراقبة
SENTRY_DSN="your-sentry-dsn"
NEXT_PUBLIC_VERCEL_ANALYTICS_ID="your-analytics-id"
النشر على Vercel:
# تثبيت Vercel CLI
npm install -g vercel

# تسجيل الدخول
vercel login

# النشر
vercel --prod

# أو استخدم تكامل Git للنشر التلقائي
git push origin main

قائمة التحقق من الإنتاج

قائمة التحقق قبل النشر:
  • ✓ جميع متغيرات البيئة مكونة في Vercel
  • ✓ تشغيل ترحيلات قاعدة البيانات على قاعدة بيانات الإنتاج
  • ✓ تكوين شهادة SSL (تلقائي مع Vercel)
  • ✓ توصيل نطاق مخصص
  • ✓ تكوين تتبع الأخطاء (Sentry)
  • ✓ تكوين التحليلات (Vercel Analytics، GA4)
  • ✓ تكوين رؤوس الأمان
  • ✓ تنفيذ تحديد معدل الطلبات
  • ✓ جدولة النسخ الاحتياطية لقاعدة البيانات
  • ✓ إعداد المراقبة والتنبيهات

تقوية الأمان

middleware.ts (تحديد المعدل والأمان):
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s'),
});

export async function middleware(request: NextRequest) {
  // تحديد معدل الطلبات لمسارات API
  if (request.nextUrl.pathname.startsWith('/api/')) {
    const ip = request.ip ?? '127.0.0.1';
    const { success } = await ratelimit.limit(ip);

    if (!success) {
      return NextResponse.json(
        { error: 'طلبات كثيرة جداً' },
        { status: 429 }
      );
    }
  }

  // رؤوس الأمان
  const response = NextResponse.next();

  response.headers.set(
    'Strict-Transport-Security',
    'max-age=31536000; includeSubDomains'
  );
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
  response.headers.set(
    'Permissions-Policy',
    'camera=(), microphone=(), geolocation=()'
  );

  return response;
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
};

المراقبة والقابلية للمراقبة

lib/monitoring.ts:
import * as Sentry from '@sentry/nextjs';

export function logError(error: Error, context?: Record<string, any>) {
  console.error(error);

  if (process.env.NODE_ENV === 'production') {
    Sentry.captureException(error, {
      extra: context,
    });
  }
}

export function logPerformance(metric: string, duration: number) {
  if (process.env.NODE_ENV === 'production') {
    // إرسال إلى خدمة التحليلات الخاصة بك
    fetch('/api/analytics/performance', {
      method: 'POST',
      body: JSON.stringify({ metric, duration }),
      keepalive: true,
    });
  }
}

التوثيق والصيانة

هيكل README.md:
# TaskFlow

تطبيق إدارة المشاريع المبني باستخدام Next.js 15

## الميزات
- المصادقة باستخدام NextAuth.js
- التعاون في الوقت الفعلي مع Pusher
- لوحات مهام السحب والإفلات
- إدارة الفريق
- الإشعارات

## مجموعة التقنيات
- Next.js 15
- TypeScript
- PostgreSQL + Prisma
- Tailwind CSS
- Pusher

## البدء
\`\`\`bash
npm install
cp .env.example .env.local
# تكوين متغيرات البيئة
npx prisma db push
npm run dev
\`\`\`

## الاختبار
\`\`\`bash
npm test              # اختبارات الوحدة
npm run test:e2e      # اختبارات E2E
\`\`\`

## النشر
انظر [DEPLOYMENT.md](./DEPLOYMENT.md)

## الترخيص
MIT
تمرين المشروع النهائي:
  1. أكمل تطبيق TaskFlow بجميع الميزات
  2. اكتب اختبارات الوحدة للمكونات الرئيسية
  3. اكتب اختبارات E2E لتدفقات المستخدم الحرجة
  4. قم بتحسين استعلامات قاعدة البيانات وإضافة الفهارس المناسبة
  5. قم بإعداد تتبع الأخطاء مع Sentry
  6. قم بتكوين CI/CD مع GitHub Actions
  7. انشر على Vercel مع نطاق مخصص
  8. قم بإعداد المراقبة والتنبيهات
  9. وثق واجهة برمجة التطبيقات الخاصة بك باستخدام Swagger/OpenAPI
  10. أنشئ README شامل
تهانينا!

لقد قمت الآن ببناء تطبيق Next.js كامل وجاهز للإنتاج من الصفر. لقد تعلمت:

  • بنية المشروع والتخطيط
  • تصميم قاعدة البيانات باستخدام Prisma
  • المصادقة باستخدام NextAuth.js
  • الميزات في الوقت الفعلي مع Pusher
  • واجهة مستخدم متقدمة مع السحب والإفلات
  • استراتيجيات الاختبار (الوحدة و E2E)
  • تحسين الأداء
  • أفضل ممارسات الأمان
  • النشر في الإنتاج
  • المراقبة والقابلية للمراقبة

تشكل هذه المهارات الأساس لبناء أي تطبيق ويب حديث باستخدام Next.js!

الخطوات التالية:
  • أضف المزيد من الميزات: الإشعارات، البحث، لوحة التحليلات
  • قم بتنفيذ قدرات PWA لدعم العمل دون اتصال
  • أضف التدويل (i18n) للعديد من اللغات
  • قم ببناء تطبيق جوال باستخدام React Native لمشاركة الكود
  • استكشف وقت تشغيل الحافة لأداء أفضل
  • قم بتنفيذ استراتيجيات تخزين مؤقت متقدمة
  • أضف ميزات الذكاء الاصطناعي باستخدام OpenAI أو واجهات برمجة التطبيقات المماثلة
  • قم ببناء تكاملات مع خدمات الطرف الثالث

اكتمل الدرس!

تهانينا! لقد أكملت جميع الدروس في هذا البرنامج التعليمي.