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

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

در این پست می‌خواهیم نسخه‌ای قابل توسعه از لیست پروژه‌ها بسازیم که نمونه کارهای ما را نشان دهد

ساخت لیست پروژه‌ها در Next.js: طراحی کارت پروژه و grid

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

  • ساختار کلی پروژه
  • Routing و Layout در Next.js 16
  • صفحه اصلی
  • Header و Footer
  • صفحه About
  • صفحه Contact

حالا وقت آن رسیده که یکی از مهم‌ترین صفحات هر پرتفولیو را بسازیم: صفحه Projects.

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

  • پروژه‌ها به‌صورت کارت نمایش داده شوند
  • layout صفحه responsive باشد
  • فعلاً از داده mock استفاده کنیم
  • اما ساختار را طوری بچینیم که بعداً به Prisma و دیتابیس وصل شود

صفحه پروژه‌ها چه نقشی دارد؟

صفحه Projects فقط یک گالری ساده نیست.
این صفحه چند نقش مهم دارد:

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

به همین دلیل، طراحی این صفحه باید هم از نظر بصری مرتب باشد، هم از نظر ساختاری قابل توسعه.

در نسخه اولیه، می‌توانیم این صفحه را شامل این بخش‌ها در نظر بگیریم:

  1. Hero کوتاه صفحه
  2. توضیح مختصر درباره نوع پروژه‌ها
  3. grid پروژه‌ها
  4. کارت مستقل برای هر پروژه

ساخت route صفحه Projects

اول فایل route اصلی را می‌سازیم:

// app/projects/page.tsx
import { ProjectsIntroSection } from '@/components/sections/projects/projects-intro-section'
import { ProjectsGridSection } from '@/components/sections/projects/projects-grid-section'

export default function ProjectsPage() {
  return (
    <div className="space-y-16">
      <ProjectsIntroSection />
      <ProjectsGridSection />
    </div>
  )
}

در اینجا هم مثل صفحه‌های قبلی، صفحه را section-based نگه داشتیم تا خوانا و قابل توسعه بماند.


Hero کوتاه برای صفحه پروژه‌ها

ابتدای صفحه باید خیلی سریع به کاربر بگوید این بخش چیست و چه چیزی قرار است ببیند.

// components/sections/projects/projects-intro-section.tsx
export function ProjectsIntroSection() {
  return (
    <section className="rounded-2xl border border-slate-800 bg-slate-900/60 p-8 sm:p-10">
      <span className="text-sm font-medium text-cyan-400">Projects</span>

      <h1 className="mt-4 text-3xl font-bold tracking-tight text-slate-50 sm:text-4xl">
        Selected work and product-focused builds
      </h1>

      <p className="mt-4 max-w-3xl text-sm leading-7 text-slate-300 sm:text-base">
        A collection of portfolio, dashboard, blog, and content-driven projects
        built with modern frontend architecture, clean UI structure, and
        scalable implementation in mind.
      </p>
    </section>
  )
}

این بخش از نظر طراحی با About و Contact هماهنگ است و consistency کلی سایت را حفظ می‌کند.


طراحی کارت پروژه

بهترین راه برای نمایش چند پروژه در یک صفحه، استفاده از card layout است.
چون کارت‌ها:

  • خوانایی خوبی دارند
  • اطلاعات را بخش‌بندی می‌کنند
  • برای grid responsive عالی هستند
  • بعداً به‌راحتی می‌توانند به دیتای واقعی یا route داینامیک وصل شوند

بهتر است کارت پروژه را به‌صورت یک کامپوننت reusable بسازیم.


اطلاعات هر پروژه

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

  • عنوان پروژه
  • توضیح کوتاه
  • تکنولوژی‌ها
  • نوع پروژه
  • لینک مشاهده جزئیات
  • لینک دمو یا گیت‌هاب
  • تصویر thumbnail در آینده

فعلاً برای شروع، حتی اگر تصویر نگذاریم هم مشکلی نیست.
مهم این است که ساختار کارت از همین حالا برای توسعه بعدی آماده باشد.


تعریف type برای پروژه

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

// types/project.ts
export type ProjectItem = {
  id: string
  slug: string
  title: string
  description: string
  category: string
  technologies: string[]
  demoUrl?: string
  githubUrl?: string
}

این type هم برای داده mock مفید است، هم بعداً وقتی داده از دیتابیس آمد، کمک می‌کند ساختار ثابتی داشته باشیم.


داده mock

تا زمانی که هنوز Prisma و دیتابیس را به صفحه Projects وصل نکرده‌ایم، از داده mock استفاده می‌کنیم.
این کار چند مزیت دارد:

  • سریع‌تر UI را می‌سازی
  • بدون وابستگی به backend می‌توانی طراحی را کامل کنیم
  • بعداً فقط منبع داده را عوض می‌کنیم
// lib/mock-projects.ts
import type { ProjectItem } from '@/types/project'

export const mockProjects: ProjectItem[] = [
  {
    id: '1',
    slug: 'portfolio-platform',
    title: 'Portfolio Platform',
    description:
      'A personal portfolio with blog, dashboard, and CMS-oriented structure built with Next.js and Tailwind CSS.',
    category: 'Portfolio',
    technologies: ['Next.js', 'React', 'Tailwind CSS', 'TypeScript'],
    demoUrl: 'https://example.com/portfolio',
    githubUrl: 'https://github.com/your-username/portfolio-platform',
  },
  {
    id: '2',
    slug: 'admin-dashboard-ui',
    title: 'Admin Dashboard UI',
    description:
      'A structured dashboard interface for managing projects, blog posts, and content workflows.',
    category: 'Dashboard',
    technologies: ['Next.js', 'Tailwind CSS', 'Zod'],
    demoUrl: 'https://example.com/dashboard',
    githubUrl: 'https://github.com/your-username/admin-dashboard-ui',
  },
  {
    id: '3',
    slug: 'blog-system',
    title: 'Blog System',
    description:
      'A blog-focused application with scalable routing, reusable sections, and clean content architecture.',
    category: 'Blog',
    technologies: ['Next.js', 'Prisma', 'PostgreSQL'],
    demoUrl: 'https://example.com/blog-system',
    githubUrl: 'https://github.com/your-username/blog-system',
  },
  {
    id: '4',
    slug: 'project-showcase',
    title: 'Project Showcase',
    description:
      'A modern project listing interface designed to present case studies and technical work in a clean format.',
    category: 'Showcase',
    technologies: ['React', 'Tailwind CSS', 'TypeScript'],
    demoUrl: 'https://example.com/project-showcase',
    githubUrl: 'https://github.com/your-username/project-showcase',
  },
]

ساخت کارت پروژه

حالا کامپوننت کارت را می‌سازیم.

// components/projects/project-card.tsx
import Link from 'next/link'
import type { ProjectItem } from '@/types/project'

type ProjectCardProps = {
  project: ProjectItem
}

export function ProjectCard({ project }: ProjectCardProps) {
  return (
    <article className="group rounded-2xl border border-slate-800 bg-slate-900/50 p-6 transition hover:border-cyan-500/40 hover:bg-slate-900">
      <div className="flex items-start justify-between gap-4">
        <div>
          <span className="inline-flex rounded-full border border-cyan-500/20 bg-cyan-500/10 px-3 py-1 text-xs font-medium text-cyan-300">
            {project.category}
          </span>

          <h2 className="mt-4 text-xl font-semibold tracking-tight text-slate-50">
            {project.title}
          </h2>
        </div>
      </div>

      <p className="mt-4 text-sm leading-7 text-slate-300">
        {project.description}
      </p>

      <div className="mt-5 flex flex-wrap gap-2">
        {project.technologies.map((tech) => (
          <span
            key={tech}
            className="rounded-md border border-slate-700 bg-slate-950 px-2.5 py-1 text-xs text-slate-300"
          >
            {tech}
          </span>
        ))}
      </div>

      <div className="mt-6 flex flex-wrap gap-3">
        <Link
          href={`/projects/${project.slug}`}
          className="inline-flex items-center justify-center rounded-md bg-cyan-500 px-4 py-2 text-sm font-semibold text-slate-950 transition hover:bg-cyan-400"
        >
          View Details
        </Link>

        {project.demoUrl ? (
          <a
            href={project.demoUrl}
            target="_blank"
            rel="noreferrer"
            className="inline-flex items-center justify-center rounded-md border border-slate-700 px-4 py-2 text-sm font-medium text-slate-200 transition hover:border-slate-600 hover:bg-slate-800"
          >
            Live Demo
          </a>
        ) : null}

        {project.githubUrl ? (
          <a
            href={project.githubUrl}
            target="_blank"
            rel="noreferrer"
            className="inline-flex items-center justify-center rounded-md border border-slate-700 px-4 py-2 text-sm font-medium text-slate-200 transition hover:border-slate-600 hover:bg-slate-800"
          >
            GitHub
          </a>
        ) : null}
      </div>
    </article>
  )
}

منطق طراحی این کارت

در این کارت چند بخش مهم داریم:

۱. category

با یک badge کوچک، نوع پروژه را نمایش می‌دهیم:

  • Portfolio
  • Dashboard
  • Blog
  • Showcase

این باعث می‌شود کاربر سریع‌تر نوع پروژه را تشخیص دهد.

۲. title

عنوان پروژه باید واضح و برجسته باشد.

۳. description

یک توضیح کوتاه و کاربردی که بگوید پروژه دقیقاً چه کاری انجام می‌دهد.

۴. technologies

نمایش تکنولوژی‌ها با badgeهای کوچک، برای پرتفولیو بسیار مهم است چون سریعاً stack پروژه را نشان می‌دهد.

۵. actions

در انتهای کارت، چند action داریم:

  • View Details برای route داخلی
  • Live Demo برای لینک خارجی
  • GitHub برای سورس کد

اینجا دوباره همان قاعده همیشگی را رعایت کردیم:

  • لینک داخلی $\rightarrow$ Link
  • لینک خارجی $\rightarrow$ a

grid responsive

حالا باید کارت‌ها را داخل یک grid responsive نمایش دهیم.
هدف این است که:

  • در موبایل یک ستونه باشند
  • در تبلت دو ستونه شوند
  • در دسکتاپ هم دو یا سه ستونه دیده شوند، بسته به نیاز طراحی

برای این پروژه، grid دو ستونه در سایز بزرگ انتخاب خوبی است چون کارت‌ها فضای کافی برای توضیح و تکنولوژی‌ها نیاز دارند.

// components/sections/projects/projects-grid-section.tsx
import { ProjectCard } from '@/components/projects/project-card'
import { mockProjects } from '@/lib/mock-projects'

export function ProjectsGridSection() {
  return (
    <section>
      <div className="max-w-2xl">
        <span className="text-sm font-medium text-cyan-400">Project List</span>
        <h2 className="mt-3 text-2xl font-bold tracking-tight text-slate-50 sm:text-3xl">
          Recent builds and selected case studies
        </h2>
        <p className="mt-4 text-sm leading-7 text-slate-300 sm:text-base">
          These projects reflect my focus on structured interfaces, modern UI
          systems, reusable components, and scalable application architecture.
        </p>
      </div>

      <div className="mt-8 grid gap-6 md:grid-cols-2">
        {mockProjects.map((project) => (
          <ProjectCard key={project.id} project={project} />
        ))}
      </div>
    </section>
  )
}

چرا md:grid-cols-2؟

چون:

  • روی موبایل، کارت‌ها یک‌ستونه خواناتر هستند
  • روی سایز متوسط و بالاتر، دو ستون تعادل خوبی بین عرض کارت و حجم محتوا می‌سازد
  • اگر توضیحات پروژه کمی بلندتر باشند، طراحی هنوز منظم باقی می‌ماند

اگر بعداً کارت‌ها کوتاه‌تر شدند یا thumbnail اضافه کردی، می‌توانی این را به xl:grid-cols-3 هم ارتقا بدهیم.


آماده‌سازی برای دیتابیس

یکی از نکات مهم این پست این است که فقط یک UI موقت نسازیم؛ بلکه ساختار را از الان برای اتصال به دیتابیس آماده کنیم.

الان جریان داده ما این است:

mockProjects -> ProjectsGridSection -> ProjectCard

بعداً خیلی راحت می‌توانی این را به این تبدیل کنیم:

Prisma query -> page.tsx or server component -> ProjectCard

مثلاً در آینده ممکن است ProjectsGridSection دیگر از mock data استفاده نکند و داده را از سرور بگیرد:

// مثال ساختاری برای آینده
import { prisma } from '@/lib/prisma'
import { ProjectCard } from '@/components/projects/project-card'

export async function ProjectsGridSection() {
  const projects = await prisma.project.findMany({
    orderBy: {
      createdAt: 'desc',
    },
  })

  return (
    <section>
      <div className="mt-8 grid gap-6 md:grid-cols-2">
        {projects.map((project) => (
          <ProjectCard key={project.id} project={project} />
        ))}
      </div>
    </section>
  )
}

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


چرا این ساختار خوب است؟

این ساختار چند مزیت مهم دارد:

  • UI و data از هم جدا هستند
  • کارت پروژه reusable است
  • داده mock به‌راحتی قابل جایگزینی است
  • grid کاملاً responsive است
  • route داخلی برای جزئیات پروژه از همین حالا پیش‌بینی شده
  • برای Prisma و CMS بعدی آماده است

ساختار فایل‌ها بعد از این مرحله

بعد از ساخت صفحه Projects، ساختار فایل‌های مربوطه می‌تواند این‌طور باشد:

app/
├── projects/
│   └── page.tsx
├── about/
│   └── page.tsx
├── contact/
│   └── page.tsx
├── blog/
│   └── page.tsx
├── layout.tsx
└── page.tsx

components/
├── projects/
│   └── project-card.tsx
├── sections/
│   ├── about/
│   ├── contact/
│   └── projects/
│       ├── projects-grid-section.tsx
│       └── projects-intro-section.tsx
├── layout/
│   ├── container.tsx
│   ├── footer.tsx
│   ├── header.tsx
│   ├── nav-link.tsx
│   └── site-nav-links.ts

lib/
├── mock-projects.ts
└── cn.ts

types/
└── project.ts

جمع‌بندی

در این پست، صفحه Projects را به‌صورت حرفه‌ای و قابل توسعه ساختیم.

چیزهایی که پیاده‌سازی کردیم:

  • route صفحه پروژه‌ها
  • intro section برای صفحه
  • type جدا برای داده پروژه
  • داده mock
  • کامپوننت reusable برای کارت پروژه
  • grid responsive برای نمایش لیست پروژه‌ها
  • آماده‌سازی ساختار برای اتصال به Prisma در آینده
// 0 comments
#Next.js#آموزش

نظرات (0)

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