MATIN MOLKARA
SERIES: آموزش Next js — PART 22 OF 28

قسمت بیست و دوم - مدیریت کامل پروژه‌ها در داشبورد ادمین Next.js

می‌خواهیم کل جریان مدیریت پروژه در داشبورد تکمیل کنیم تا ادمین بتواند لیست پروژه ها را ببیند، پروژه جدید بسازد، پروژه ها رو ویرایش یا حذف کند

مدیریت کامل پروژه‌ها در داشبورد ادمین Next.js

تا اینجای آموزش، چند قطعه مهم از پنل ادمین را ساخته‌ایم:

  • احراز هویت ادمین
  • محافظت از مسیرهای پنل
  • ساختار کلی داشبورد
  • APIهای مربوط به CRUD پروژه‌ها

حالا وقت آن رسیده که این بخش را واقعاً کامل کنیم.

در این قسمت، قرار نیست فقط یک لیست ساده از پروژه‌ها داشته باشیم.
می‌خواهیم کل جریان مدیریت پروژه در داشبورد را ببندیم؛ یعنی ادمین بتواند:

  • لیست پروژه‌ها را ببیند
  • پروژه جدید بسازد
  • پروژه موجود را ویرایش کند
  • پروژه را حذف کند
  • و همه این عملیات را از داخل پنل ادمین انجام دهد

به زبان ساده، هدف این قسمت این است که بخش Projects در داشبورد از حالت نیمه‌کاره خارج شود و به یک ماژول کامل مدیریت محتوا تبدیل شود.


در این قسمت چه چیزی می‌سازیم؟

در پایان این آموزش، این مسیرها را خواهیم داشت:

/admin/projects
/admin/projects/new
/admin/projects/[id]/edit

و از نظر قابلیت‌ها، این موارد کامل می‌شوند:

  • صفحه لیست پروژه‌ها
  • جدول مدیریت پروژه‌ها
  • دکمه ساخت پروژه جدید
  • فرم ساخت پروژه
  • فرم ویرایش پروژه
  • حذف پروژه
  • اتصال کامل به Route Handlers
  • ریدایرکت بعد از عملیات موفق
  • مدیریت خطاها در سطح فرم

نگاه کلی به ساختار فایل‌ها

قبل از شروع، بهتر است تصویر کلی فایل‌ها را ببینیم:

app/
  admin/
    (dashboard)/
      projects/
        page.tsx
        new/
          page.tsx
        [id]/
          edit/
            page.tsx

  api/
    projects/
      route.ts
      [id]/
        route.ts

components/
  admin/
    projects-table.tsx
    project-form.tsx
    delete-project-button.tsx

lib/
  actions/
    project-actions.ts
  validations/
    project.ts

اگر اسم بعضی فایل‌ها در پروژه شما کمی متفاوت است، مسئله‌ای نیست.
مهم منطق و ارتباط بخش‌هاست.


معماری این بخش

برای کامل‌کردن مدیریت پروژه‌ها در داشبورد، بهتر است این الگو را دنبال کنیم:

خواندن داده

با Server Component و Prisma

عملیات تغییر

از طریق فرم‌ها و Route Handlers

رفرش رابط کاربری

با redirect() یا router.refresh()

محافظت

با requireAdmin()

این الگو در Next.js App Router تمیز، مقیاس‌پذیر و قابل فهم است.


بخش اول: صفحه لیست پروژه‌ها

اول از همه باید صفحه اصلی مدیریت پروژه‌ها را داشته باشیم:

/admin/projects

این صفحه وظیفه دارد:

  • همه پروژه‌ها را از دیتابیس بخواند
  • آن‌ها را در جدول نمایش دهد
  • لینک ساخت پروژه جدید را نشان دهد
  • برای هر پروژه، عملیات ویرایش و حذف را فراهم کند

کد صفحه لیست پروژه‌ها

// app/admin/(dashboard)/projects/page.tsx
import Link from 'next/link'
import { prisma } from '@/lib/prisma'
import { requireAdmin } from '@/lib/auth/session'
import { ProjectsTable } from '@/components/admin/projects-table'

export default async function AdminProjectsPage() {
  await requireAdmin()

  const projects = await prisma.project.findMany({
    orderBy: {
      createdAt: 'desc',
    },
    select: {
      id: true,
      title: true,
      slug: true,
      summary: true,
      published: true,
      createdAt: true,
      updatedAt: true,
    },
  })

  return (
    <section className="space-y-6">
      <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
        <div>
          <h1 className="text-3xl font-semibold tracking-tight text-white">
            Projects
          </h1>
          <p className="mt-2 text-sm text-zinc-400">
            Create, edit, and manage your portfolio projects.
          </p>
        </div>

        <Link
          href="/admin/projects/new"
          className="inline-flex items-center justify-center rounded-xl bg-white px-4 py-2 text-sm font-medium text-black transition hover:bg-zinc-200"
        >
          New Project
        </Link>
      </div>

      <ProjectsTable projects={projects} />
    </section>
  )
}

بخش دوم: جدول مدیریت پروژه‌ها

برای نمایش پروژه‌ها بهتر است از یک جدول استفاده کنیم.
چون هم خواناست، هم مناسب پنل ادمین، و هم برای عملیات مدیریتی طبیعی‌تر است.


کامپوننت جدول پروژه‌ها

// components/admin/projects-table.tsx
import Link from 'next/link'
import { DeleteProjectButton } from '@/components/admin/delete-project-button'

type ProjectRow = {
  id: string
  title: string
  slug: string
  summary: string | null
  published: boolean
  createdAt: Date
  updatedAt: Date
}

type ProjectsTableProps = {
  projects: ProjectRow[]
}

export function ProjectsTable({ projects }: ProjectsTableProps) {
  if (projects.length === 0) {
    return (
      <div className="rounded-2xl border border-dashed border-zinc-800 bg-zinc-950 p-10 text-center">
        <h2 className="text-lg font-medium text-white">No projects yet</h2>
        <p className="mt-2 text-sm text-zinc-400">
          Start by creating your first project in the dashboard.
        </p>

        <Link
          href="/admin/projects/new"
          className="mt-5 inline-flex items-center justify-center rounded-xl bg-white px-4 py-2 text-sm font-medium text-black transition hover:bg-zinc-200"
        >
          Create Project
        </Link>
      </div>
    )
  }

  return (
    <div className="overflow-hidden rounded-2xl border border-zinc-800 bg-black">
      <table className="min-w-full divide-y divide-zinc-800">
        <thead className="bg-zinc-950">
          <tr>
            <th className="px-4 py-3 text-left text-sm font-medium text-zinc-400">
              Title
            </th>
            <th className="px-4 py-3 text-left text-sm font-medium text-zinc-400">
              Slug
            </th>
            <th className="px-4 py-3 text-left text-sm font-medium text-zinc-400">
              Summary
            </th>
            <th className="px-4 py-3 text-left text-sm font-medium text-zinc-400">
              Status
            </th>
            <th className="px-4 py-3 text-left text-sm font-medium text-zinc-400">
              Updated
            </th>
            <th className="px-4 py-3 text-left text-sm font-medium text-zinc-400">
              Actions
            </th>
          </tr>
        </thead>

        <tbody className="divide-y divide-zinc-800">
          {projects.map((project) => (
            <tr key={project.id}>
              <td className="px-4 py-3 text-sm font-medium text-white">
                {project.title}
              </td>

              <td className="px-4 py-3 text-sm text-zinc-400">
                {project.slug}
              </td>

              <td className="max-w-xs truncate px-4 py-3 text-sm text-zinc-400">
                {project.summary || '-'}
              </td>

              <td className="px-4 py-3 text-sm">
                <span className="rounded-full border border-zinc-700 px-2.5 py-1 text-xs text-zinc-300">
                  {project.published ? 'Published' : 'Draft'}
                </span>
              </td>

              <td className="px-4 py-3 text-sm text-zinc-400">
                {project.updatedAt.toLocaleDateString()}
              </td>

              <td className="px-4 py-3 text-sm">
                <div className="flex items-center gap-4">
                  <Link
                    href={`/admin/projects/${project.id}/edit`}
                    className="text-zinc-300 transition hover:text-white"
                  >
                    Edit
                  </Link>

                  <DeleteProjectButton projectId={project.id} />
                </div>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}

این جدول سه حالت مهم را پوشش می‌دهد:

  • اگر پروژه‌ای وجود نداشته باشد، وضعیت خالی را نشان می‌دهد
  • اگر پروژه‌ها وجود داشته باشند، آن‌ها را به‌صورت منظم نمایش می‌دهد
  • برای هر ردیف، عملیات مدیریتی مشخص دارد

بخش سوم: اعتبارسنجی داده‌های پروژه

قبل از ساخت فرم، بهتر است schema پروژه را مشخص کنیم.
این بخش باعث می‌شود هم ساخت و هم ویرایش از یک منطق مشترک استفاده کنند.


تعریف projectSchema

// lib/validations/project.ts
import { z } from 'zod'

export const projectSchema = z.object({
  title: z.string().trim().min(1, 'Title is required').max(120),
  slug: z
    .string()
    .trim()
    .min(1, 'Slug is required')
    .max(160)
    .regex(/^[a-z0-9-]+$/, 'Slug must contain only lowercase letters, numbers, and hyphens'),
  summary: z.string().trim().max(300).optional().or(z.literal('')),
  content: z.string().trim().min(1, 'Content is required'),
  published: z.boolean(),
})

export type ProjectInput = z.infer<typeof projectSchema>

اگر در مدل پروژه شما فیلدهای بیشتری وجود دارد، مثل:

  • coverImage
  • images
  • demoUrl
  • githubUrl
  • techStack

می‌توانیم آن‌ها را هم به schema اضافه کنیم.
اما برای این آموزش، فرم را روی ستون‌های اصلی نگه می‌داریم تا مدیریت پروژه کامل و واضح بماند.


بخش چهارم: فرم مشترک برای ساخت و ویرایش

یکی از بهترین کارها این است که به‌جای ساخت دو فرم جدا، یک فرم مشترک داشته باشیم که هم برای ساخت پروژه و هم برای ویرایش پروژه استفاده شود.

مزایای این کار:

  • تکرار کد کمتر می‌شود
  • ظاهر فرم در هر دو صفحه یکسان می‌ماند
  • نگهداری کد ساده‌تر می‌شود

کامپوننت ProjectForm

// components/admin/project-form.tsx
'use client'

import { useState } from 'react'
import { useRouter } from 'next/navigation'

type ProjectFormValues = {
  title: string
  slug: string
  summary: string
  content: string
  published: boolean
}

type ProjectFormProps = {
  mode: 'create' | 'edit'
  initialValues?: ProjectFormValues
  projectId?: string
}

export function ProjectForm({
  mode,
  initialValues,
  projectId,
}: ProjectFormProps) {
  const router = useRouter()

  const [title, setTitle] = useState(initialValues?.title ?? '')
  const [slug, setSlug] = useState(initialValues?.slug ?? '')
  const [summary, setSummary] = useState(initialValues?.summary ?? '')
  const [content, setContent] = useState(initialValues?.content ?? '')
  const [published, setPublished] = useState(initialValues?.published ?? false)

  const [isSubmitting, setIsSubmitting] = useState(false)
  const [error, setError] = useState<string | null>(null)

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    setError(null)
    setIsSubmitting(true)

    try {
      const payload = {
        title,
        slug,
        summary,
        content,
        published,
      }

      const response =
        mode === 'create'
          ? await fetch('/api/projects', {
              method: 'POST',
              headers: {
                'Content-Type': 'application/json',
              },
              body: JSON.stringify(payload),
            })
          : await fetch(`/api/projects/${projectId}`, {
              method: 'PATCH',
              headers: {
                'Content-Type': 'application/json',
              },
              body: JSON.stringify(payload),
            })

      const data = await response.json().catch(() => null)

      if (!response.ok) {
        throw new Error(data?.error || 'Failed to save project')
      }

      router.push('/admin/projects')
      router.refresh()
    } catch (error) {
      setError(
        error instanceof Error ? error.message : 'Something went wrong'
      )
    } finally {
      setIsSubmitting(false)
    }
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-6 rounded-2xl border border-zinc-800 bg-zinc-950 p-6">
      <div className="space-y-2">
        <label htmlFor="title" className="block text-sm font-medium text-white">
          Title
        </label>
        <input
          id="title"
          type="text"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          className="w-full rounded-xl border border-zinc-800 bg-black px-4 py-3 text-sm text-white outline-none transition placeholder:text-zinc-500 focus:border-zinc-600"
          placeholder="Project title"
        />
      </div>

      <div className="space-y-2">
        <label htmlFor="slug" className="block text-sm font-medium text-white">
          Slug
        </label>
        <input
          id="slug"
          type="text"
          value={slug}
          onChange={(e) => setSlug(e.target.value)}
          className="w-full rounded-xl border border-zinc-800 bg-black px-4 py-3 text-sm text-white outline-none transition placeholder:text-zinc-500 focus:border-zinc-600"
          placeholder="project-slug"
        />
      </div>

      <div className="space-y-2">
        <label htmlFor="summary" className="block text-sm font-medium text-white">
          Summary
        </label>
        <textarea
          id="summary"
          value={summary}
          onChange={(e) => setSummary(e.target.value)}
          rows={3}
          className="w-full rounded-xl border border-zinc-800 bg-black px-4 py-3 text-sm text-white outline-none transition placeholder:text-zinc-500 focus:border-zinc-600"
          placeholder="Short project summary"
        />
      </div>

      <div className="space-y-2">
        <label htmlFor="content" className="block text-sm font-medium text-white">
          Content
        </label>
        <textarea
          id="content"
          value={content}
          onChange={(e) => setContent(e.target.value)}
          rows={10}
          className="w-full rounded-xl border border-zinc-800 bg-black px-4 py-3 text-sm text-white outline-none transition placeholder:text-zinc-500 focus:border-zinc-600"
          placeholder="Write the full project content"
        />
      </div>

      <label className="flex items-center gap-3 text-sm text placeholder:text-zinc-500 focus:border-zinc-600"
          placeholder="Write the full project content"
        />
      </div>

      <label className="flex items-center gap-3 text-sm text-zinc-300">
        <input
          type="checkbox"
          checked={published}
          onChange={(e) => setPublished(e.target border border-red-900/50 bg-red-950/30 px-4 py-3 text-sm text-red-300">
          {error}
        </div>
      ) : null}

      <div className="flex items-center gap-3">
        <button
          type="submit"
          disabled={isSubmitting}
          className="inline-flex items-center justify-center rounded-xl bg-white px-4 py-2 text-sm font-medium text-black transition hover:bg-zinc-200 disabled:cursor-not-allowed disabled:opacity-60"
        >
          {isSubmitting
            ? mode === 'create'
              ? 'Creating...'
              : 'Saving...'
            : mode === 'create'
            ? 'Create Project'
            : 'Save Changes'}
        </button>
      </div>
    </form>
  )
}

این فرم همه بخش‌های اصلی را پوشش می‌دهد:

  • عنوان
  • اسلاگ
  • خلاصه
  • محتوا
  • وضعیت انتشار
  • خطا
  • وضعیت ارسال

و مهم‌تر از همه، برای هر دو حالت create و edit کار می‌کند.


بخش پنجم: صفحه ساخت پروژه جدید

حالا که فرم آماده است، صفحه ساخت پروژه جدید بسیار ساده می‌شود.


صفحه /admin/projects/new

// app/admin/(dashboard)/projects/new/page.tsx
import Link from 'next/link'
import { requireAdmin } from '@/lib/auth/session'
import { ProjectForm } from '@/components/admin/project-form'

export default async function NewProjectPage() {
  await requireAdmin()

  return (
    <section className="space-y-6">
      <div className="space-y-2">
        <Link
          href="/admin/projects"
          className="text-sm text-zinc-400 transition hover:text-white"
        >
          Back to Projects
        </Link>

        <h1 className="text-3xl font-semibold tracking-tight text-white">
          New Project
        </h1>

        <p className="text-sm text-zinc-400">
          Create a new project for your portfolio.
        </p>
      </div>

      <ProjectForm mode="create" />
    </section>
  )
}

در این صفحه:

  • فقط ادمین واردشده اجازه دسترسی دارد
  • یک لینک بازگشت به لیست داریم
  • فرم در حالت ساخت استفاده می‌شود

بخش ششم: صفحه ویرایش پروژه

برای ویرایش، باید ابتدا پروژه موردنظر را از دیتابیس بخوانیم و بعد داده‌های آن را به فرم بدهیم.


صفحه /admin/projects/[id]/edit

// app/admin/(dashboard)/projects/[id]/edit/page.tsx
import { notFound } from 'next/navigation'
import Link from 'next/link'
import { prisma } from '@/lib/prisma'
import { requireAdmin } from '@/lib/auth/session'
import { ProjectForm } from '@/components/admin/project-form'

type EditProjectPageProps = {
  params: Promise<{
    id: string
  }>
}

export default async function EditProjectPage({
  params,
}: EditProjectPageProps) {
  await requireAdmin()

  const { id } = await params

  const project = await prisma.project.findUnique({
    where: { id },
    select: {
      id: true,
      title: true,
      slug: true,
      summary: true,
      content: true,
      published: true,
    },
  })

  if (!project) {
    notFound()
  }

  return (
    <section className="space-y-6">
      <div className="space-y-2">
        <Link
          href="/admin/projects"
          className="text-sm text-zinc-400 transition hover:text-white"
        >
          Back to Projects
        </Link>

        <h1 className="text-3xl font-semibold tracking-tight text-white">
          Edit Project
        </h1>

        <p className="text-sm text-zinc-400">
          Update the selected project details.
        </p>
      </div>

      <ProjectForm
        mode="edit"
        projectId={project.id}
        initialValues={{
          title: project.title,
          slug: project.slug,
          summary: project.summary ?? '',
          content: project.content,
          published: project.published,
        }}
      />
    </section>
  )
}

این صفحه دقیقاً چیزی را کامل می‌کند که در لیست پروژه‌ها با لینک Edit شروع کرده بودیم.


بخش هفتم: حذف پروژه

حذف پروژه نیاز به یک Client Component دارد، چون:

  • باید رویداد کلیک را کنترل کند
  • باید درخواست DELETE بفرستد
  • باید بعد از موفقیت UI را تازه‌سازی کند

دکمه حذف پروژه

// components/admin/delete-project-button.tsx
'use client'

import { useRouter } from 'next/navigation'
import { useState } from 'react'

type DeleteProjectButtonProps = {
  projectId: string
}

export function DeleteProjectButton({
  projectId,
}: DeleteProjectButtonProps) {
  const router = useRouter()
  const [isDeleting, setIsDeleting] = useState(false)

  async function handleDelete() {
    const confirmed = window.confirm(
      'Are you sure you want to delete this project?'
    )

    if (!confirmed) return

    setIsDeleting(true)

    try {
      const response = await fetch(`/api/projects/${projectId}`, {
        method: 'DELETE',
      })

      const data = await response.json().catch(() => null)

      if (!response.ok) {
        throw new Error(data?.error || 'Failed to delete project')
      }

      router.refresh()
    } catch (error) {
      window.alert(
        error instanceof Error ? error.message : 'Something went wrong'
      )
    } finally {
      setIsDeleting(false)
    }
  }

  return (
    <button
      type="button"
      onClick={handleDelete}
      disabled={isDeleting}
      className="text-red-400 transition hover:text-red-300 disabled:cursor-not-allowed disabled:opacity-60"
    >
      {isDeleting ? 'Deleting...' : 'Delete'}
    </button>
  )
}

این نسخه برای پنل ادمین کاملاً مناسب است و رفتار پایه حذف را کامل می‌کند.


بخش هشتم: Route Handler ساخت پروژه

حالا باید مطمئن شویم فرم ساخت پروژه به endpoint درست وصل است.


فایل app/api/projects/route.ts

// app/api/projects/route.ts
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { requireAdminFromRequest } from '@/lib/auth/session'
import { projectSchema } from '@/lib/validations/project'

export async function POST(request: Request) {
  try {
    await requireAdminFromRequest()

    const body = await request.json()
    const parsed = projectSchema.safeParse(body)

    if (!parsed.success) {
      return NextResponse.json(
        {
          error: 'Invalid project data',
          issues: parsed.error.flatten(),
        },
        { status: 400 }
      )
    }

    const data = parsed.data

    const existingProject = await prisma.project.findUnique({
      where: {
        slug: data.slug,
      },
      select: {
        id: true,
      },
    })

    if (existingProject) {
      return NextResponse.json(
        { error: 'A project with this slug already exists' },
        { status: 409 }
      )
    }

    const project = await prisma.project.create({
      data: {
        title: data.title,
        slug: data.slug,
        summary: data.summary || null,
        content: data.content,
        published: data.published,
      },
    })

    return NextResponse.json(project, { status: 201 })
  } catch (error) {
    console.error('POST /api/projects error:', error)

    return NextResponse.json(
      { error: 'Failed to create project' },
      { status: 500 }
    )
  }
}

بخش نهم: Route Handler ویرایش و حذف پروژه

برای ویرایش و حذف، از فایل مسیر داینامیک استفاده می‌کنیم.


فایل app/api/projects/[id]/route.ts

// app/api/projects/[id]/route.ts
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { requireAdminFromRequest } from '@/lib/auth/session'
import { projectSchema } from '@/lib/validations/project'

type RouteContext = {
  params: Promise<{
    id: string
  }>
}

export async function PATCH(request: Request, { params }: RouteContext) {
  try {
    await requireAdminFromRequest()

    const { id } = await params
    const body = await request.json()
    const parsed = projectSchema.safeParse(body)

    if (!parsed.success) {
      return NextResponse.json(
        {
          error: 'Invalid project data',
          issues: parsed.error.flatten(),
        },
        { status: 400 }
      )
    }

    const data = parsed.data

    const project = await prisma.project.findUnique({
      where: { id },
      select: { id: true, slug: true },
    })

    if (!project) {
      return NextResponse.json(
        { error: 'Project not found' },
        { status: 404 }
      )
    }

    const existingSlug = await prisma.project.findFirst({
      where: {
        slug: data.slug,
        NOT: {
          id,
        },
      },
      select: {
        id: true,
      },
    })

    if (existingSlug) {
      return NextResponse.json(
        { error: 'Another project already uses this slug' },
        { status: 409 }
      )
    }

    const updatedProject = await prisma.project.update({
      where: { id },
      data: {
        title: data.title,
        slug: data.slug,
        summary: data.summary || null,
        content: data.content,
        published: data.published,
      },
    })

    return NextResponse.json(updatedProject)
  } catch (error) {
    console.error('PATCH /api/projects/[id] error:', error)

    return NextResponse.json(
      { error: 'Failed to update project' },
      { status: 500 }
    )
  }
}

export async function DELETE(_: Request, { params }: RouteContext) {
  try {
    await requireAdminFromRequest()

    const { id } = await params

    const project = await prisma.project.findUnique({
      where: { id },
      select: { id: true },
    })

    if (!project) {
      return NextResponse.json(
        { error: 'Project not found' },
        { status: 404 }
      )
    }

    await prisma.project.delete({
      where: { id },
    })

    return NextResponse.json({ success: true })
  } catch (error) {
    console.error('DELETE /api/projects/[id] error:', error)

    return NextResponse.json(
      { error: 'Failed to delete project' },
      { status: 500 }
    )
  }
}

بخش دهم: اتصال کامل جریان مدیریت پروژه

تا اینجا کل جریان مدیریت پروژه در داشبورد کامل شده است.


1. لیست پروژه‌ها

ادمین وارد این مسیر می‌شود:

/admin/projects

و همه پروژه‌ها را در جدول می‌بیند.


2. ساخت پروژه جدید

روی دکمه New Project کلیک می‌کند و وارد این مسیر می‌شود:

/admin/projects/new

فرم را پر می‌کند، درخواست به این endpoint می‌رود:

POST /api/projects

و بعد از موفقیت، کاربر به لیست پروژه‌ها برمی‌گردد.


3. ویرایش پروژه

روی Edit کلیک می‌کند و وارد این مسیر می‌شود:

/admin/projects/[id]/edit

فرم با داده‌های فعلی پروژه پر می‌شود، سپس درخواست به این endpoint می‌رود:

PATCH /api/projects/[id]

و بعد از ذخیره، دوباره به لیست بازمی‌گردد.


4. حذف پروژه

روی Delete کلیک می‌کند، تأیید می‌کند و درخواست به این endpoint می‌رود:

DELETE /api/projects/[id]

بعد از موفقیت، جدول دوباره refresh می‌شود.


نکات مهم این پیاده‌سازی

1. فرم ساخت و ویرایش یکی است

این کار باعث می‌شود کد تمیزتر و قابل نگهداری‌تر بماند.

2. خواندن داده در Server Component انجام می‌شود

برای لیست و صفحه ویرایش، این روش کاملاً مناسب است.

3. عملیات تغییر از طریق API انجام می‌شوند

ساخت، ویرایش و حذف همگی از طریق Route Handlers انجام می‌شوند.

4. تمام عملیات مدیریتی محافظت شده‌اند

هم صفحات ادمین و هم APIها session ادمین را بررسی می‌کنند.

5. مسیرها قابل پیش‌بینی‌اند

ساختار بسیار واضح است:

/admin/projects
/admin/projects/new
/admin/projects/[id]/edit

اگر مدل پروژه شما فیلدهای بیشتری دارد چه می‌شود؟

اگر در پروژه واقعی‌ات فیلدهای بیشتری داری، مثل:

  • coverImage
  • images
  • demoUrl
  • githubUrl
  • technologies

همین ساختار حفظ می‌شود.
فقط باید:

  • schema را گسترش بدهی
  • فرم را بزرگ‌تر کنی
  • و داده‌ها را در create و update ذخیره کنی

یعنی معماری این بخش عوض نمی‌شود؛ فقط فرم و مدل غنی‌تر می‌شوند.


نتیجه این قسمت

با این آموزش، بخش Projects در داشبورد ادمین عملاً کامل شد.

یعنی حالا پنل ادمین ما فقط یک ظاهر ساده ندارد، بلکه یکی از مهم‌ترین ماژول‌های محتوایی آن کاملاً قابل استفاده است:

  • نمایش پروژه‌ها
  • ساخت پروژه جدید
  • ویرایش پروژه
  • حذف پروژه

این دقیقاً همان چیزی است که از یک داشبورد مدیریت واقعی انتظار داریم.


جمع‌بندی

در این قسمت، مدیریت پروژه‌ها را در داشبورد ادمین به‌صورت کامل پیاده‌سازی کردیم و حلقه بین backend و admin UI را بستیم.

یاد گرفتیم که:

  • صفحه /admin/projects مرکز مدیریت پروژه‌هاست
  • پروژه‌ها را در جدول نمایش می‌دهیم
  • دکمه New Project کاربر را به فرم ساخت می‌برد
  • لینک Edit مسیر ویرایش را باز می‌کند
  • دکمه Delete حذف پروژه را از داخل پنل انجام می‌دهد
  • برای ساخت و ویرایش از یک فرم مشترک استفاده می‌کنیم
  • Route Handlerها منطق نهایی ساخت، ویرایش و حذف را مدیریت می‌کنند
  • همه این مسیرها و endpointها باید محافظت‌شده باشند
// 0 comments
#Next.js#اموزش

نظرات (0)

نظر خود را بنویسید