إطار Next.js
بناء تطبيق Next.js كامل - الجزء الأول
بناء تطبيق Next.js كامل - الجزء الأول
في هذه السلسلة الشاملة المكونة من جزأين، سنبني تطبيق Next.js كامل وجاهز للإنتاج من الصفر. يركز الجزء الأول على تخطيط المشروع، والإعداد الأولي، والمصادقة، وتصميم قاعدة البيانات، وتنفيذ الميزات الأساسية.
نظرة عامة على المشروع: TaskFlow - تطبيق إدارة المشاريع
سنبني TaskFlow، وهو تطبيق حديث لإدارة المشاريع بالميزات التالية:
- المصادقة: تسجيل المستخدم، وتسجيل الدخول، وإدارة الجلسات
- المشاريع: إنشاء وإدارة وأرشفة المشاريع
- المهام: إنشاء المهام، والتعيين، وتتبع الحالة، والأولويات
- الفرق: التعاون الجماعي وإدارة الأعضاء
- التعليقات: تعليقات المهام في الوقت الفعلي والمناقشات
- لوحة المعلومات: التحليلات ونظرة عامة على المشروع
- الإشعارات: تحديثات في الوقت الفعلي لأنشطة الفريق
الخطوة 1: تخطيط المشروع والبنية
مجموعة التقنيات:
- الإطار: Next.js 15 مع App Router
- اللغة: TypeScript
- قاعدة البيانات: PostgreSQL مع Prisma ORM
- المصادقة: NextAuth.js v5 (Auth.js)
- التنسيق: Tailwind CSS + shadcn/ui
- إدارة الحالة: Zustand لحالة العميل
- الوقت الفعلي: Pusher للتحديثات المباشرة
- النشر: Vercel
هيكل المشروع:
taskflow/
├── app/
│ ├── (auth)/
│ │ ├── login/
│ │ └── register/
│ ├── (dashboard)/
│ │ ├── dashboard/
│ │ ├── projects/
│ │ │ ├── [id]/
│ │ │ └── new/
│ │ ├── tasks/
│ │ └── teams/
│ ├── api/
│ │ ├── auth/
│ │ ├── projects/
│ │ ├── tasks/
│ │ └── notifications/
│ └── layout.tsx
├── components/
│ ├── ui/ # مكونات shadcn
│ ├── layout/ # مكونات التخطيط
│ ├── projects/ # مكونات خاصة بالمشروع
│ └── tasks/ # مكونات خاصة بالمهام
├── lib/
│ ├── auth.ts # تكوين المصادقة
│ ├── db.ts # عميل Prisma
│ ├── utils.ts # وظائف المساعدة
│ └── validations/ # مخططات Zod
├── prisma/
│ └── schema.prisma
└── types/
└── index.ts
الخطوة 2: الإعداد الأولي للمشروع
إنشاء مشروع Next.js:
npx create-next-app@latest taskflow --typescript --tailwind --app --use-npm cd taskflow
تثبيت التبعيات:
# التبعيات الأساسية npm install prisma @prisma/client npm install next-auth@beta npm install zod react-hook-form @hookform/resolvers npm install zustand npm install pusher pusher-js npm install date-fns npm install lucide-react # مكونات واجهة المستخدم npx shadcn-ui@latest init npx shadcn-ui@latest add button input label card dialog dropdown-menu
متغيرات البيئة (.env.local):
# قاعدة البيانات DATABASE_URL="postgresql://user:password@localhost:5432/taskflow" # المصادقة NEXTAUTH_URL="http://localhost:3000" NEXTAUTH_SECRET="your-secret-key-here" # OAuth (اختياري) GOOGLE_CLIENT_ID="your-google-client-id" GOOGLE_CLIENT_SECRET="your-google-client-secret" # Pusher NEXT_PUBLIC_PUSHER_KEY="your-pusher-key" PUSHER_SECRET="your-pusher-secret" PUSHER_APP_ID="your-app-id" NEXT_PUBLIC_PUSHER_CLUSTER="mt1"
الخطوة 3: تصميم مخطط قاعدة البيانات
prisma/schema.prisma:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
name String?
email String @unique
emailVerified DateTime?
image String?
password String?
role Role @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accounts Account[]
sessions Session[]
projects ProjectMember[]
tasks Task[]
comments Comment[]
notifications Notification[]
}
enum Role {
USER
ADMIN
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
model Project {
id String @id @default(cuid())
name String
description String?
slug String @unique
color String @default("#3B82F6")
archived Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
members ProjectMember[]
tasks Task[]
}
model ProjectMember {
id String @id @default(cuid())
role MemberRole @default(MEMBER)
joinedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
projectId String
@@unique([userId, projectId])
}
enum MemberRole {
OWNER
ADMIN
MEMBER
}
model Task {
id String @id @default(cuid())
title String
description String?
status TaskStatus @default(TODO)
priority TaskPriority @default(MEDIUM)
dueDate DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
projectId String
assignee User? @relation(fields: [assigneeId], references: [id])
assigneeId String?
comments Comment[]
}
enum TaskStatus {
TODO
IN_PROGRESS
IN_REVIEW
DONE
}
enum TaskPriority {
LOW
MEDIUM
HIGH
URGENT
}
model Comment {
id String @id @default(cuid())
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
taskId String
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId String
}
model Notification {
id String @id @default(cuid())
type String
title String
message String
read Boolean @default(false)
link String?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
}
تهيئة قاعدة البيانات:
npx prisma generate npx prisma db push
الخطوة 4: إعداد المصادقة
lib/auth.ts:
import NextAuth from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import Google from 'next-auth/providers/google';
import { PrismaAdapter } from '@auth/prisma-adapter';
import { prisma } from '@/lib/db';
import bcrypt from 'bcryptjs';
import { z } from 'zod';
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(6),
});
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma),
session: { strategy: 'jwt' },
pages: {
signIn: '/login',
error: '/login',
},
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
Credentials({
credentials: {
email: { label: 'البريد الإلكتروني', type: 'email' },
password: { label: 'كلمة المرور', type: 'password' },
},
async authorize(credentials) {
const validated = loginSchema.safeParse(credentials);
if (!validated.success) {
return null;
}
const { email, password } = validated.data;
const user = await prisma.user.findUnique({
where: { email },
});
if (!user || !user.password) {
return null;
}
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return null;
}
return {
id: user.id,
email: user.email,
name: user.name,
image: user.image,
};
},
}),
],
callbacks: {
async session({ session, token }) {
if (token.sub) {
session.user.id = token.sub;
}
return session;
},
async jwt({ token, user }) {
if (user) {
token.sub = user.id;
}
return token;
},
},
});
app/api/auth/[...nextauth]/route.ts:
import { handlers } from '@/lib/auth';
export const { GET, POST } = handlers;
app/(auth)/login/page.tsx:
'use client';
import { signIn } from 'next-auth/react';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card } from '@/components/ui/card';
export default function LoginPage() {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
setError('');
const formData = new FormData(e.currentTarget);
const email = formData.get('email') as string;
const password = formData.get('password') as string;
try {
const result = await signIn('credentials', {
email,
password,
redirect: false,
});
if (result?.error) {
setError('البريد الإلكتروني أو كلمة المرور غير صحيحة');
} else {
router.push('/dashboard');
}
} catch (error) {
setError('حدث خطأ');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center">
<Card className="w-full max-w-md p-6">
<h1 className="text-2xl font-bold mb-6">تسجيل الدخول إلى TaskFlow</h1>
{error && (
<div className="bg-red-50 text-red-600 p-3 rounded mb-4">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Label htmlFor="email">البريد الإلكتروني</Label>
<Input
id="email"
name="email"
type="email"
required
autoComplete="email"
/>
</div>
<div>
<Label htmlFor="password">كلمة المرور</Label>
<Input
id="password"
name="password"
type="password"
required
autoComplete="current-password"
/>
</div>
<Button type="submit" className="w-full" disabled={loading}>
{loading ? 'جاري تسجيل الدخول...' : 'تسجيل الدخول'}
</Button>
</form>
<div className="mt-4">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-white px-2 text-gray-500">
أو تابع باستخدام
</span>
</div>
</div>
<Button
type="button"
variant="outline"
className="w-full mt-4"
onClick={() => signIn('google', { callbackUrl: '/dashboard' })}
>
تابع باستخدام Google
</Button>
</div>
</Card>
</div>
);
}
الخطوة 5: نماذج البيانات الأساسية ومسارات API
lib/validations/project.ts:
import { z } from 'zod';
export const createProjectSchema = z.object({
name: z.string().min(1, 'اسم المشروع مطلوب').max(100),
description: z.string().optional(),
color: z.string().regex(/^#[0-9A-F]{6}$/i).optional(),
});
export const updateProjectSchema = createProjectSchema.partial();
export type CreateProjectInput = z.infer<typeof createProjectSchema>;
export type UpdateProjectInput = z.infer<typeof updateProjectSchema>;
app/api/projects/route.ts:
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { prisma } from '@/lib/db';
import { createProjectSchema } from '@/lib/validations/project';
export async function GET(request: NextRequest) {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: 'غير مصرح' }, { status: 401 });
}
const projects = await prisma.project.findMany({
where: {
members: {
some: {
userId: session.user.id,
},
},
archived: false,
},
include: {
members: {
include: {
user: {
select: {
id: true,
name: true,
image: true,
},
},
},
},
_count: {
select: {
tasks: true,
},
},
},
orderBy: {
updatedAt: 'desc',
},
});
return NextResponse.json(projects);
}
export async function POST(request: NextRequest) {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: 'غير مصرح' }, { status: 401 });
}
const body = await request.json();
const validated = createProjectSchema.safeParse(body);
if (!validated.success) {
return NextResponse.json(
{ error: validated.error.errors },
{ status: 400 }
);
}
const { name, description, color } = validated.data;
// إنشاء المعرف
const slug = name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
const project = await prisma.project.create({
data: {
name,
description,
color,
slug: \`${slug}-${Date.now()}\`,
members: {
create: {
userId: session.user.id,
role: 'OWNER',
},
},
},
include: {
members: {
include: {
user: true,
},
},
},
});
return NextResponse.json(project, { status: 201 });
}
تمرين - نقطة التفتيش للجزء الأول:
- قم بإعداد مشروع Next.js جديد باستخدام مجموعة التقنيات المذكورة أعلاه
- قم بتكوين مخطط قاعدة البيانات وتشغيل الترحيلات
- قم بتنفيذ المصادقة باستخدام بيانات الاعتماد و Google OAuth
- أنشئ مسارات API للمشاريع مع نقاط نهاية GET و POST
- قم ببناء صفحة تسجيل الدخول مع معالجة الأخطاء المناسبة
- اختبر تسجيل المستخدم وتسجيل الدخول وإنشاء المشروع
نصائح التطوير:
- استخدم متغيرات البيئة لجميع التكوينات الحساسة
- اختبر تدفقات المصادقة بدقة قبل بناء الميزات
- حافظ على مخطط قاعدة البيانات الخاص بك طبيعياً ومفهرساً بشكل جيد
- استخدم أنواع TypeScript المُنشأة من مخططات Prisma
- قم بتنفيذ معالجة الأخطاء المناسبة من البداية
- استخدم Prisma Studio (npx prisma studio) لفحص قاعدة البيانات الخاصة بك
ما التالي في الجزء الثاني:
في الجزء الثاني، سنستمر في بناء TaskFlow من خلال تنفيذ:
- إدارة المهام مع لوحات السحب والإفلات
- التعاون في الوقت الفعلي مع Pusher
- الميزات المتقدمة (الإشعارات، البحث، الفلاتر)
- استراتيجيات الاختبار وتنفيذ الاختبار
- تقنيات تحسين الأداء
- النشر في الإنتاج والمراقبة
- تقوية الأمان وأفضل الممارسات