Next.js: المشروع النهائي: CRUD و Server Actions
آخر تحديث: 2026-08-26
إدارة المهام هي جوهر أي منصة SaaS — إنشاء المهام وتحريرها وحذفها وسحبها وإفلاتها لإعادة ترتيبها ورفع الملفات والبحث والتصفية. يمكّن نظام CRUD الشامل المستخدمين من إدارة سير العمل بكفاءة.
1. ما ستتعلمه
- تنفيذ CRUD المهام (إنشاء / تحديث / حذف) باستخدام Server Actions
- التحقق عبر Zod + وضع التحديث المتفائل
useOptimistic - نافذة تأكيد
useActionStateومعالجة الأخطاء - تنفيذ الفرز بالسحب والإفلات للتبديل بين أعمدة "المهام" و"قيد التنفيذ" و"مكتملة"
- ربط رفع ملفات Uploadthing بنموذج Attachment
- معاملات بحث URL + shadcn/ui DataTable لتنفيذ البحث والتصفية والترقيم
2. قصة حقيقية لمدير مشروع
(1) نقطة الألم: فريق مكون من 50 شخصا يهدر 3 ساعات يوميا في إدارة المهام
بوب هو مدير مشروع في شركة Acme Corp يدير 5 مشاريع وأكثر من 200 مهمة. يقضي الفريق وقتا كبيرا يوميا في: إنشاء المهام (ملء نماذج من 10 حقول بشكل متكرر)، والسحب والإفلات لتحديث الحالات (مثل لصق أوراق الملاحظات على لوحة بيضاء)، وطلب الملفات من الزملاء (تمريرها ذهابا وإيابا على WeChat)، والبحث عن المهام (التمرير عبر جداول Excel). حسبت أليس أنه في فريق مكون من 50 شخصا، يهدر كل شخص 36 دقيقة يوميا في إدارة مهام غير فعالة، مما ينتج عنه خسارة إنتاجية شهرية تبلغ 600 ساعة.
(2) Server Actions + التحديثات المتفائلة + حل للسحب والإفلات
تنفيذ CRUD بدون تحديث باستخدام Server Actions + useOptimistic للتغذية الراجعة الفورية + تبديل الحالة بالسحب والإفلات.
// سطر واحد من Server Action لإكمال إنشاء المهمة + تحديث التخزين المؤقت
<form action={createTask}>
<input name="title" required />
<input name="projectId" type="hidden" value={project.id} />
<button type="submit">إنشاء مهمة</button>
</form>
(3) المكاسب
| السيناريو | قبل التحسين | بعد التحسين | تحسين الكفاءة |
|---|---|---|---|
| إنشاء مهمة | 3 دقائق (نموذج كامل) | 15 ثانية (إنشاء سريع) | 12x |
| الفرز بالسحب والإفلات | لوحة بيضاء مكتوبة يدويا | النقر على عمود الحالة | فوري |
| مشاركة الملفات | إرسال الملفات عبر WeChat | الرفع عبر Uploadthing | 5x |
| البحث عن مهمة | تصفح Excel | بحث DataTable فوري | 20x |
| الإنتاجية الشهرية | 600 ساعة مهدرة | ~50 ساعة مهدرة | تقليل بنسبة 92% |
3. CRUD المهام باستخدام Server Actions
(1) هندسة العمليات
graph TB
subgraph "Server Actions"
A[createTask] --> B[التحقق عبر Zod]
B --> C[إنشاء Prisma]
C --> D[revalidateTag]
D --> E[redirect]
end
subgraph "العميل"
F[TaskForm] -->|action| A
G[useOptimistic] -->|واجهة مستخدم فورية| F
H[useActionState] -->|تأكيد| J[deleteTask]
end
subgraph "التخزين المؤقت"
D --> K[revalidateTag<br/>"tasks"]
K --> L[تحديث واجهة المستخدم]
end
style A fill:#cce5ff
style G fill:#d4edda
style K fill:#ffeeba
(2) مقارنة عمليات المهام
| العملية | Server Action | التحقق | سياسة التخزين المؤقت | تغذية واجهة المستخدم |
|---|---|---|---|---|
| createTask | "use server" |
Zod | revalidateTag("tasks") |
useOptimistic |
| updateTask | "use server" |
Zod جزئي | revalidateTag("tasks") |
تحديث متفائل |
| deleteTask | "use server" |
فحص التأكيد | revalidateTag("tasks") |
useActionState |
| reorderTask | "use server" |
التحقق من الرقم | revalidateTag("tasks") |
إعادة ترتيب فورية |
▶ مثال: تعريف Server Actions
المخرجات:
رسم توضيحي: createTask؛ التحقق عبر Zod؛ إنشاء Prisma؛ revalidateTag؛ redirect؛ TaskForm.
// src/lib/actions/task-actions.ts
"use server"
import { revalidateTag } from "next/cache"
import { redirect } from "next/navigation"
import { z } from "zod"
import { prisma } from "@/lib/db"
import { auth } from "@/lib/auth"
import { TaskStatus, TaskPriority } from "@prisma/client"
const createTaskSchema = z.object({
title: z.string().min(1, "العنوان مطلوب").max(200, "العنوان طويل جدا"),
description: z.string().max(2000).optional(),
projectId: z.string().min(1),
assigneeId: z.string().optional(),
priority: z.nativeEnum(TaskPriority).optional(),
dueDate: z.string().optional(),
})
const updateTaskSchema = z.object({
id: z.string().min(1),
title: z.string().min(1).max(200).optional(),
description: z.string().max(2000).optional(),
status: z.nativeEnum(TaskStatus).optional(),
priority: z.nativeEnum(TaskPriority).optional(),
assigneeId: z.string().nullable().optional(),
dueDate: z.string().nullable().optional(),
order: z.number().int().optional(),
})
export async function createTask(formData: FormData) {
const session = await auth()
if (!session?.user?.id) {
throw new Error("غير مصرح")
}
const rawData = {
title: formData.get("title") as string,
description: formData.get("description") as string | undefined,
projectId: formData.get("projectId") as string,
assigneeId: formData.get("assigneeId") as string | undefined,
priority: (formData.get("priority") as TaskPriority) || "MEDIUM",
dueDate: formData.get("dueDate") as string | undefined,
}
const validated = createTaskSchema.safeParse(rawData)
if (!validated.success) {
return {
error: validated.error.flatten().fieldErrors,
message: "فشل التحقق",
}
}
const data = validated.data
const maxOrder = await prisma.task.findFirst({
where: { projectId: data.projectId, status: "TODO" },
orderBy: { order: "desc" },
select: { order: true },
})
await prisma.task.create({
data: {
title: data.title,
description: data.description || null,
status: "TODO",
priority: data.priority || "MEDIUM",
projectId: data.projectId,
creatorId: session.user.id,
assigneeId: data.assigneeId || null,
dueDate: data.dueDate ? new Date(data.dueDate) : null,
order: (maxOrder?.order ?? -1) + 1,
},
})
revalidateTag("tasks")
revalidateTag(`project-${data.projectId}`)
}
export async function updateTask(formData: FormData) {
const session = await auth()
if (!session?.user?.id) {
throw new Error("غير مصرح")
}
const rawData = {
id: formData.get("id") as string,
title: formData.get("title") as string | undefined,
description: formData.get("description") as string | undefined,
status: formData.get("status") as TaskStatus | undefined,
priority: formData.get("priority") as TaskPriority | undefined,
assigneeId: formData.get("assigneeId") as string | null | undefined,
dueDate: formData.get("dueDate") as string | null | undefined,
}
const validated = updateTaskSchema.safeParse(rawData)
if (!validated.success) {
return {
error: validated.error.flatten().fieldErrors,
message: "فشل التحقق",
}
}
const data = validated.data
const updateData: Record<string, unknown> = {}
if (data.title !== undefined) updateData.title = data.title
if (data.description !== undefined) updateData.description = data.description
if (data.status !== undefined) updateData.status = data.status
if (data.priority !== undefined) updateData.priority = data.priority
if (data.assigneeId !== undefined) updateData.assigneeId = data.assigneeId
if (data.dueDate !== undefined) updateData.dueDate = data.dueDate ? new Date(data.dueDate) : null
if (data.order !== undefined) updateData.order = data.order
await prisma.task.update({
where: { id: data.id },
data: updateData,
})
revalidateTag("tasks")
}
export async function deleteTask(prevState: { message: string }, formData: FormData) {
const session = await auth()
if (!session?.user?.id) {
return { message: "غير مصرح" }
}
const taskId = formData.get("id") as string
if (!taskId) {
return { message: "معرف المهمة مطلوب" }
}
const task = await prisma.task.findUnique({
where: { id: taskId },
select: { creatorId: true, projectId: true },
})
if (!task) {
return { message: "المهمة غير موجودة" }
}
if (task.creatorId !== session.user.id) {
return { message: "يمكن للمنشئ فقط حذف هذه المهمة" }
}
await prisma.task.delete({ where: { id: taskId } })
revalidateTag("tasks")
revalidateTag(`project-${task.projectId}`)
return { message: "تم حذف المهمة بنجاح" }
}
export async function reorderTask(taskId: string, newStatus: TaskStatus, newOrder: number) {
const session = await auth()
if (!session?.user?.id) {
throw new Error("غير مصرح")
}
await prisma.task.update({
where: { id: taskId },
data: { status: newStatus, order: newOrder },
})
revalidateTag("tasks")
}
المخرجات:
يقبل حقول النموذج: title، description، projectId، assigneeId، priority.
يتحقق من الإدخال باستخدام Zod schema قبل المعالجة.
يكتب في قاعدة البيانات ويبطل صلاحية وسوم التخزين المؤقت ذات الصلة.
4. التحديثات المتفائلة ونوافذ التأكيد المنبثقة
(1) ثلاثة أوضاع لواجهة المستخدم
graph LR
subgraph "التحديث المتفائل"
A1[نقر المستخدم] --> B1[تحديث فوري لواجهة المستخدم]
B1 --> C1[Server Action]
C1 --> D1{تراجع؟}
D1 -->|نجاح| E1[إبقاء]
D1 -->|خطأ| F1[استعادة]
end
subgraph "حالة الإجراء"
A2[نقر المستخدم على حذف] --> B2[إظهار حوار التأكيد]
B2 --> C2[إذا تم التأكيد<br/>استدعاء deleteTask]
C2 --> D2[إظهار رسالة النتيجة]
end
subgraph "التقليدي"
A3[إرسال النموذج] --> B3[أيقونة تحميل دوارة]
B3 --> C3[Server Action]
C3 --> D3[revalidateTag + تحديث]
end
style B1 fill:#d4edda
style B2 fill:#ffeeba
style B3 fill:#f8d7da
▶ مثال: useOptimistic — التحديثات المتفائلة
المخرجات:
يوضح الرسم التوضيحي الهندسة/التدفق الموصوف.
// src/components/tasks/task-list.tsx
"use client"
import { useOptimistic } from "react"
import { TaskCard } from "@/components/tasks/task-card"
import type { TaskWithAssignee } from "@/types"
interface TaskListProps {
tasks: TaskWithAssignee[]
status: "TODO" | "IN_PROGRESS" | "DONE"
onStatusChange: (taskId: string, newStatus: "TODO" | "IN_PROGRESS" | "DONE") => void
}
export function TaskList({ tasks, status, onStatusChange }: TaskListProps) {
const [optimisticTasks, addOptimisticTask] = useOptimistic(
tasks,
(state, newTask: TaskWithAssignee) => {
return [newTask, ...state]
}
)
return (
<div className="space-y-2">
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
{status.replace("_", " ")}
<span className="ml-2 text-xs">({optimisticTasks.length})</span>
</h3>
{optimisticTasks.length === 0 ? (
<div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
لا توجد مهام
</div>
) : (
<div className="space-y-2">
{optimisticTasks.map((task) => (
<TaskCard
key={task.id}
task={task}
onStatusChange={onStatusChange}
/>
))}
</div>
)}
</div>
)
}
المخرجات:
يعرض واجهة مستخدم مكون ▶ مثال: useOptimistic — التحديثات المتفائلة كما هو موضح في القسم.
▶ مثال: useActionState لتأكيد الحذف
المخرجات:
يعرض المكون واجهة المستخدم الموصوفة في المتصفح.
// src/components/tasks/delete-task-button.tsx
"use client"
import { useActionState, useState } from "react"
import { deleteTask } from "@/lib/actions/task-actions"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
interface DeleteTaskButtonProps {
taskId: string
taskTitle: string
}
export function DeleteTaskButton({ taskId, taskTitle }: DeleteTaskButtonProps) {
const [open, setOpen] = useState(false)
const [state, formAction, isPending] = useActionState(deleteTask, {
message: "",
})
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-destructive">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</Button>
</DialogTrigger>
<DialogContent>
<form action={formAction}>
<input type="hidden" name="id" value={taskId} />
<DialogHeader>
<DialogTitle>حذف المهمة</DialogTitle>
<DialogDescription>
هل أنت متأكد من رغبتك في حذف "{taskTitle}"؟ لا يمكن التراجع عن هذا الإجراء.
</DialogDescription>
</DialogHeader>
{state.message && state.message !== "تم حذف المهمة بنجاح" && (
<div className="my-4 rounded-md bg-destructive/10 p-3 text-sm text-destructive">
{state.message}
</div>
)}
{state.message === "تم حذف المهمة بنجاح" && (
<div className="my-4 rounded-md bg-green-50 p-3 text-sm text-green-700">
{state.message}
</div>
)}
<DialogFooter className="mt-4">
<Button
type="button"
variant="outline"
onClick={() => setOpen(false)}
>
إلغاء
</Button>
<Button
type="submit"
variant="destructive"
disabled={isPending}
>
{isPending ? "جاري الحذف..." : "حذف"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
المخرجات:
نموذج تفاعلي مع إدارة حالة من جانب العميل.
المحتوى المرئي: حذف المهمة
5. الفرز بالسحب والإفلات (لوحة كانبان ثلاثية الأعمدة)
(1) هيكل كانبان
graph LR
subgraph "لوحة المهام"
A["المهام<br/>(عمود)"] -->|سحب| B["قيد التنفيذ<br/>(عمود)"]
B -->|سحب| C["مكتملة<br/>(عمود)"]
C -->|سحب للخلف| B
B -->|سحب للخلف| A
end
style A fill:#fef3c7
style B fill:#dbeafe
style C fill:#d1fae5
▶ مثال: تخطيط ثلاثي الأعمدة للوحة المهام
المخرجات:
رسم توضيحي للتخطيط وهيكل القوالب في دليل app.
// src/components/tasks/task-board.tsx
"use client"
import { useCallback } from "react"
import { TaskList } from "@/components/tasks/task-list"
import { reorderTask } from "@/lib/actions/task-actions"
import type { TaskWithAssignee } from "@/types"
interface TaskBoardProps {
tasks: TaskWithAssignee[]
}
export function TaskBoard({ tasks }: TaskBoardProps) {
const todoTasks = tasks.filter((t) => t.status === "TODO")
const inProgressTasks = tasks.filter((t) => t.status === "IN_PROGRESS")
const doneTasks = tasks.filter((t) => t.status === "DONE")
const handleStatusChange = useCallback(
async (taskId: string, newStatus: "TODO" | "IN_PROGRESS" | "DONE") => {
const targetList = tasks.filter((t) => t.status === newStatus)
const maxOrder = targetList.length > 0
? Math.max(...targetList.map((t) => t.order))
: 0
try {
await reorderTask(taskId, newStatus, maxOrder + 1)
} catch (error) {
console.error("فشل إعادة ترتيب المهمة:", error)
}
},
[tasks]
)
return (
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
<div className="rounded-lg bg-muted/50 p-4">
<TaskList
tasks={todoTasks}
status="TODO"
onStatusChange={handleStatusChange}
/>
</div>
<div className="rounded-lg bg-muted/50 p-4">
<TaskList
tasks={inProgressTasks}
status="IN_PROGRESS"
onStatusChange={handleStatusChange}
/>
</div>
<div className="rounded-lg bg-muted/50 p-4">
<TaskList
tasks={doneTasks}
status="DONE"
onStatusChange={handleStatusChange}
/>
</div>
</div>
)
}
المخرجات:
يعرض واجهة مستخدم مكون ▶ مثال: تخطيط ثلاثي الأعمدة للوحة المهام كما هو موضح في القسم.
▶ مثال: مكون بطاقة المهمة
المخرجات:
يعرض المكون واجهة المستخدم الموصوفة في المتصفح.
// src/components/tasks/task-card.tsx
"use client"
import { DeleteTaskButton } from "@/components/tasks/delete-task-button"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import type { TaskWithAssignee } from "@/types"
interface TaskCardProps {
task: TaskWithAssignee
onStatusChange: (taskId: string, newStatus: "TODO" | "IN_PROGRESS" | "DONE") => void
}
const statusActions: Record<string, ("TODO" | "IN_PROGRESS" | "DONE")[]> = {
TODO: ["IN_PROGRESS"],
IN_PROGRESS: ["TODO", "DONE"],
DONE: ["IN_PROGRESS"],
}
const priorityColors: Record<string, string> = {
LOW: "bg-gray-100 text-gray-700",
MEDIUM: "bg-blue-100 text-blue-700",
HIGH: "bg-orange-100 text-orange-700",
URGENT: "bg-red-100 text-red-700",
}
export function TaskCard({ task, onStatusChange }: TaskCardProps) {
const initials = task.assignee?.name
?.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
return (
<div className="group rounded-lg border bg-card p-3 shadow-sm transition-all hover:shadow-md">
<div className="flex items-start justify-between">
<h4 className="text-sm font-medium">{task.title}</h4>
<DeleteTaskButton taskId={task.id} taskTitle={task.title} />
</div>
{task.description && (
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
{task.description}
</p>
)}
<div className="mt-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<Badge
variant="secondary"
className={priorityColors[task.priority]}
>
{task.priority}
</Badge>
{task.dueDate && (
<span className="text-xs text-muted-foreground">
{new Date(task.dueDate).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
})}
</span>
)}
</div>
<div className="flex items-center gap-2">
<div className="flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
{statusActions[task.status].map((action) => (
<button
key={action}
onClick={() => onStatusChange(task.id, action)}
className="rounded px-2 py-1 text-[10px] font-medium uppercase transition-colors hover:bg-accent"
>
{action === "TODO"
? "\u21A9"
: action === "IN_PROGRESS"
? "\u25B6"
: "\u2713"}
</button>
))}
</div>
{task.assignee && (
<Avatar className="h-6 w-6">
<AvatarImage src={task.assignee.image ?? undefined} />
<AvatarFallback className="text-[10px]">
{initials}
</AvatarFallback>
</Avatar>
)}
</div>
</div>
{task._count?.comments > 0 && (
<div className="mt-2 flex items-center gap-1 text-xs text-muted-foreground">
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
{task._count.comments}
</div>
)}
</div>
)
}
المخرجات:
يعرض واجهة مستخدم مكون ▶ مثال: مكون بطاقة المهمة كما هو موضح في القسم.
6. رفع الملفات (uploadthing)
(1) هندسة الرفع
sequenceDiagram
participant C as العميل
participant U as UploadThing
participant S as Server Action
participant DB as قاعدة البيانات
C->>U: رفع الملف (multipart)
U-->>C: إرجاع fileUrl + بيانات وصفية
C->>S: Server Action(formData مع fileUrl)
S->>DB: إنشاء سجل Attachment
DB-->>S: تم إنشاء المرفق
S->>S: revalidateTag("tasks")
S-->>C: نجاح
▶ مثال: إعداد UploadThing
المخرجات:
رسم توضيحي متسلسل يظهر تدفق الرسائل بين المشاركين.
// src/lib/uploadthing.ts
import { createUploadthing, type FileRouter } from "uploadthing/next"
import { auth } from "@/lib/auth"
const f = createUploadthing()
export const ourFileRouter = {
taskAttachment: f({
image: { maxFileSize: "4MB", maxFileCount: 5 },
pdf: { maxFileSize: "8MB", maxFileCount: 3 },
"text/*": { maxFileSize: "2MB", maxFileCount: 5 },
})
.middleware(async () => {
const session = await auth()
if (!session?.user) throw new Error("غير مصرح")
return { userId: session.user.id }
})
.onUploadComplete(async ({ metadata, file }) => {
return { uploadedBy: metadata.userId, fileUrl: file.url }
}),
} satisfies FileRouter
export type OurFileRouter = typeof ourFileRouter
المخرجات:
يعرف أنواع TypeScript: FileRouter، OurFileRouter.
// src/app/api/uploadthing/core.ts
import { createRouteHandler } from "uploadthing/next"
import { ourFileRouter } from "@/lib/uploadthing"
export const { GET, POST } = createRouteHandler({
router: ourFileRouter,
})
7. البحث والتصفية والترقيم
(1) هيكل معاملات بحث URL
graph LR
A[معاملات بحث URL] --> B[مكون الخادم]
B --> C[استعلام Prisma]
C --> D[نتائج مصفاة]
D --> E[shadcn/ui DataTable]
E -->|تفاعل المستخدم| F[تحديث معاملات URL]
F -->|تنقل Next.js| A
style A fill:#cce5ff
style E fill:#d4edda
style F fill:#ffeeba
8. مثال كامل: صفحة لوحة المهام الكاملة
// src/app/(dashboard)/dashboard/projects/[id]/page.tsx
import { Suspense } from "react"
import { prisma } from "@/lib/db"
import { auth } from "@/lib/auth"
import { notFound } from "next/navigation"
import { TaskBoard } from "@/components/tasks/task-board"
import { CreateTaskForm } from "@/components/tasks/create-task-form"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
interface ProjectBoardPageProps {
params: Promise<{ id: string }>
}
export default async function ProjectBoardPage({ params }: ProjectBoardPageProps) {
const { id } = await params
const session = await auth()
const project = await prisma.project.findUnique({
where: { id },
include: {
_count: { select: { tasks: true } },
},
})
if (!project) {
notFound()
}
const tasks = await prisma.task.findMany({
where: { projectId: id },
orderBy: { order: "asc" },
include: {
assignee: { select: { name: true, image: true } },
_count: { select: { comments: true, attachments: true } },
},
})
const statusCounts = {
TODO: tasks.filter((t) => t.status === "TODO").length,
IN_PROGRESS: tasks.filter((t) => t.status === "IN_PROGRESS").length,
DONE: tasks.filter((t) => t.status === "DONE").length,
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-3">
<div
className="h-4 w-4 rounded-full"
style={{ backgroundColor: project.color }}
/>
<h1 className="text-2xl font-bold">{project.name}</h1>
<Badge variant="secondary">
{project.status}
</Badge>
</div>
<p className="mt-1 text-sm text-muted-foreground">
{tasks.length} مهام · {statusCounts.TODO} مهام ·{" "}
{statusCounts.IN_PROGRESS} قيد التنفيذ · {statusCounts.DONE} مكتملة
</p>
</div>
</div>
<Suspense fallback={<div className="h-12 animate-pulse rounded-lg bg-muted" />}>
<CreateTaskForm projectId={project.id} members={[]} />
</Suspense>
<TaskBoard tasks={tasks} />
</div>
)
}
المخرجات المتوقعة (عند الوصول إلى /dashboard/projects/[id] في المتصفح):
→ لوحة كانبان ثلاثية الأعمدة: مهام (8) | قيد التنفيذ (5) | مكتملة (7)
→ النقر على زر "+" لإظهار نموذج الإنشاء السريع → إدخال عنوان → Enter → تظهر المهمة فورا في عمود المهام (تحديث متفائل)
→ النقر على زر ▶ في بطاقة المهمة → نقل المهمة إلى قيد التنفيذ
→ النقر على حذف → عرض حوار التأكيد → تأكيد الحذف → اختفت المهمة
→ سحب وإفلات الملفات في منطقة الرفع → عرض شريط التقدم → ربط المرفقات بالمهمة
→ البحث عن "auth" → تحديث معامل URL → تصفية DataTable لعرض المهام المطابقة
❓ أسئلة شائعة
useOptimistic و useState العادي؟useOptimistic هو hook جديد تم تقديمه في React 19، مصمم خصيصا لنمط التحديث المتفائل المستخدم مع Server Actions. يقوم بتحديث واجهة المستخدم فورا أثناء تنفيذ Server Action ويعود تلقائيا إلى البيانات الفعلية إذا فشل Server Action. استخدام useState مع التراجع اليدوي يتطلب كودا إضافيا أكثر ويكون عرضة للأخطاء./uploads/ المحلي سهل التنفيذ ولكنه غير مناسب لـ production (لا يوجد CDN، مساحة تخزين محدودة، وصعوبة في النسخ الاحتياطي). يستخدم TaskFlow الرفع المحلي كحل للتطوير وينتقل إلى uploadthing في بيئة production.safeParse الآمن من حيث الأنواع، والذي يولد رسائل خطأ type تلقائيا، مما يجعله أكثر موثوقية من التحقق اليدوي بـ if-else.useActionState لحذف مهمة بدلا من استدعاء deleteTask مباشرة؟useActionState هو hook مصمم لـ Server Actions في React 19. يوفر حالة isPending (لتعطيل الزر ومنع الإرسالات المكررة) ودالة action المغلفة (لمعالجة formData تلقائيا). مقارنة بالاستدعاء المباشر، فإن useActionState أكثر أمانا (حماية CSRF) ويوفر تغذية راجعة أكثر طبيعية لواجهة المستخدم.📖 ملخص
- Server Actions: استخدم أمر
"use server"لتنفيذ الدوال مباشرة على الخادم، متجنبا كتابة API Routes يدويا - يوفر Zod تحققا آمنا من حيث الأنواع للنماذج، وعند دمجه مع معالجة أخطاء Server Actions، يتيح سير عمل تحقق كامل.
useOptimisticيمكّن تحديثات فورية لواجهة المستخدم ويعود تلقائيا عند فشل Server ActionuseActionStateيدير حالة نوافذ التأكيد المنبثقة، موفراisPendingوتغليفا تلقائيا لـ formData- لوحة كانبان ثلاثية الأعمدة تنفذ تبديل الحالة بالسحب والإفلات عبر Server Action
reorderTask - Uploadthing +
/uploads/المحلي — رفع ملفات بطريقتين، يدعم الصور وPDF والنصوص - معاملات بحث URL تمكّن الترقيم الحقيقي والبحث والتصفية؛ DataTable يعرض معلومات المهام الكاملة عبر 7 أعمدة
revalidateTag("tasks")يوحد استراتيجيات إبطال التخزين المؤقت لضمان تحديث فوري لواجهة المستخدم بعد عمليات CRUD
📝 تمارين
-
تمرين أساسي (⭐): نفذ نموذج إنشاء مهمة سريع (Title + projectId) في مشروع TaskFlow باستخدام Server Action وتحديث متفائل. التحقق: بعد النقر على "إنشاء"، تظهر المهمة فورا في عمود المهام بلوحة كانبان.
-
تمرين متقدم (⭐⭐): أضف ميزة "عملية جماعية" إلى DataTable — بعد تحديد عدة صفوف، قم بتحديث حالتها جماعيا (TODO → IN_PROGRESS) أو حذفها جماعيا. استخدم
useOptimisticلتنفيذ تحديثات متفائلة للتحديدات الجماعية. اكتب Server ActionbulkUpdateTasksللتعامل مع معاملات المصفوفة. -
تحدي (⭐⭐⭐): نفذ فرز كامل بالسحب والإفلات (باستخدام
@dnd-kit/coreأو HTML5 Drag & Drop API الأصلي): اسمح بسحب بطاقات المهام بحرية بين الأعمدة الثلاثة؛ اعرض عنصرا نائبا أثناء السحب؛ استدعreorderTaskبعد تحرير البطاقة لتحديث الحالة وترتيب الفرز؛ استخدمuseOptimisticلتوفير تغذية راجعة فورية للفرز بعد السحب؛ تعامل مع تعارضات السحب (القفل المتفائل عندما يسحب مستخدمان نفس المهمة في نفس الوقت).