قسمت هشتم - ساخت لیست پروژهها در Next.js
در این پست میخواهیم نسخهای قابل توسعه از لیست پروژهها بسازیم که نمونه کارهای ما را نشان دهد
ساخت لیست پروژهها در Next.js: طراحی کارت پروژه و grid
تا اینجای مسیر، ما بخشهای مهمی از پرتفولیوی خودمان را ساختهایم:
- ساختار کلی پروژه
- Routing و Layout در Next.js 16
- صفحه اصلی
- Header و Footer
- صفحه About
- صفحه Contact
حالا وقت آن رسیده که یکی از مهمترین صفحات هر پرتفولیو را بسازیم: صفحه Projects.
در این پست میخواهیم نسخهای قابل توسعه از لیست پروژهها بسازیم؛ با این هدف که:
- پروژهها بهصورت کارت نمایش داده شوند
- layout صفحه responsive باشد
- فعلاً از داده mock استفاده کنیم
- اما ساختار را طوری بچینیم که بعداً به Prisma و دیتابیس وصل شود
صفحه پروژهها چه نقشی دارد؟
صفحه Projects فقط یک گالری ساده نیست.
این صفحه چند نقش مهم دارد:
- نمایش نمونهکارها
- نشان دادن تنوع مهارتها
- هدایت کاربر به جزئیات هر پروژه
- ایجاد اعتماد حرفهای
- آماده بودن برای تبدیل شدن به یک بخش dynamic در آینده
به همین دلیل، طراحی این صفحه باید هم از نظر بصری مرتب باشد، هم از نظر ساختاری قابل توسعه.
در نسخه اولیه، میتوانیم این صفحه را شامل این بخشها در نظر بگیریم:
- Hero کوتاه صفحه
- توضیح مختصر درباره نوع پروژهها
- grid پروژهها
- کارت مستقل برای هر پروژه
ساخت 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 در آینده