Add CMS website and Qom landing pages with responsive layout fixes.

Ship blog/portfolio routes, shared content components, and mobile-safe typography so the marketing site can deploy cleanly.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-07-29 21:57:44 +03:30
co-authored by Cursor
parent 47bee1ce66
commit 112e5eb46d
29 changed files with 1584 additions and 147 deletions
+112
View File
@@ -0,0 +1,112 @@
import type { Metadata } from "next";
import Link from "next/link";
import { ContentPageShell } from "@/components/ContentPageShell";
import { Pagination } from "@/components/Pagination";
import { getPortfolioCoverImage, getPortfolios } from "@/lib/api";
export const metadata: Metadata = {
title: "نمونه‌کارها | مشکی",
description: "نمونه پروژه‌ها و کارهای انجام‌شده توسط تیم مشکی.",
};
const PAGE_SIZE = 12;
type PortfolioPageProps = {
searchParams: Promise<{ page?: string }>;
};
export default async function PortfolioListPage({
searchParams,
}: PortfolioPageProps) {
const { page: pageParam } = await searchParams;
const page = Math.max(1, Number(pageParam) || 1);
let items: Awaited<ReturnType<typeof getPortfolios>>["items"] = [];
let total = 0;
try {
const data = await getPortfolios({ page, pageSize: PAGE_SIZE });
items = data.items;
total = data.total;
} catch {
items = [];
total = 0;
}
return (
<ContentPageShell>
<section className="pb-20 lg:pb-28">
<div className="mx-auto max-w-7xl px-5 lg:px-8">
<div className="reveal mb-12 max-w-2xl md:mb-16">
<h1 className="whitespace-normal text-[clamp(1.35rem,3.6vw,2.75rem)] font-normal tracking-tight text-ink">
نمونه‌کارها
</h1>
<p className="latin mt-3 text-xs uppercase tracking-[0.2em] text-muted">
Portfolio
</p>
<p className="mt-5 text-sm leading-8 text-muted md:text-base">
نگاهی به پروژه‌ها و کارهای انجام‌شده توسط تیم مشکی.
</p>
</div>
{items.length === 0 ? (
<p className="text-sm text-muted">
نمونه‌کاری برای نمایش وجود ندارد.
</p>
) : (
<div key={page} className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
{items.map((item, index) => {
const image = getPortfolioCoverImage(item);
return (
<article
key={item.id}
className={`reveal group ${
index % 3 === 1
? "reveal-delay-1"
: index % 3 === 2
? "reveal-delay-2"
: ""
}`}
>
<Link href={`/portfolio/${item.slug}`} className="block">
<div className="relative mb-4 overflow-hidden rounded-2xl bg-paper-deep">
{image ? (
<img
src={image}
alt=""
className="h-auto w-full transition duration-700 group-hover:scale-105"
/>
) : null}
</div>
{item.categoryName ? (
<p className="latin mb-2 text-[10px] uppercase tracking-widest text-muted">
{item.categoryName}
</p>
) : null}
<h2 className="group-text-hover mb-2 line-clamp-2 whitespace-normal text-lg font-normal leading-8 text-ink">
{item.titleFa || item.title}
</h2>
{item.abstract ? (
<p className="line-clamp-3 text-sm leading-7 text-muted">
{item.abstract}
</p>
) : null}
</Link>
</article>
);
})}
</div>
)}
<Pagination
page={page}
pageSize={PAGE_SIZE}
total={total}
basePath="/portfolio"
/>
</div>
</section>
</ContentPageShell>
);
}