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
+134
View File
@@ -0,0 +1,134 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { ContentPageShell } from "@/components/ContentPageShell";
import { HtmlContent } from "@/components/HtmlContent";
import {
ApiNotFoundError,
formatFaDate,
getPortfolio,
getPortfolioCoverImage,
} from "@/lib/api";
type PortfolioDetailPageProps = {
params: Promise<{ slug: string }>;
};
export async function generateMetadata({
params,
}: PortfolioDetailPageProps): Promise<Metadata> {
const { slug } = await params;
try {
const portfolio = await getPortfolio(slug);
const title = portfolio.titleFa || portfolio.title;
return {
title: `${title} | مشکی`,
description: portfolio.abstract ?? undefined,
};
} catch {
return { title: "نمونه‌کار | مشکی" };
}
}
export default async function PortfolioDetailPage({
params,
}: PortfolioDetailPageProps) {
const { slug } = await params;
let portfolio;
try {
portfolio = await getPortfolio(slug);
} catch (error) {
if (error instanceof ApiNotFoundError) notFound();
throw error;
}
const cover = getPortfolioCoverImage(portfolio);
const date = formatFaDate(portfolio.publishedAt);
const title = portfolio.titleFa || portfolio.title;
const gallery = (portfolio.gallery ?? []).filter(
(item) => item.url !== cover,
);
return (
<ContentPageShell>
<article className="pb-20 lg:pb-28">
<div className="mx-auto max-w-5xl px-5 lg:px-8">
<div className="reveal mb-10 max-w-3xl">
<Link
href="/portfolio"
className="link-hover mb-6 inline-flex text-sm text-muted"
>
بازگشت به نمونه‌کارها
</Link>
{portfolio.categoryName ? (
<p className="latin mb-3 text-[11px] uppercase tracking-widest text-muted">
{portfolio.categoryName}
</p>
) : null}
<h1 className="whitespace-normal text-[clamp(1.35rem,3.8vw,2.5rem)] font-normal leading-tight tracking-tight text-ink">
{title}
</h1>
{portfolio.titleEn ? (
<p className="latin mt-3 text-xs uppercase tracking-widest text-muted">
{portfolio.titleEn}
</p>
) : null}
{portfolio.abstract ? (
<p className="mt-5 text-sm leading-8 text-muted md:text-base">
{portfolio.abstract}
</p>
) : null}
{date ? (
<time
className="mt-5 block text-xs text-muted"
dateTime={portfolio.publishedAt ?? undefined}
>
{date}
</time>
) : null}
</div>
{cover ? (
<div className="reveal reveal-delay-1 mb-10 overflow-hidden rounded-2xl bg-paper-deep">
<img
src={cover}
alt=""
className="mx-auto max-h-[32rem] w-full object-contain"
/>
</div>
) : null}
{gallery.length > 0 ? (
<div className="reveal reveal-delay-2 mb-12 grid gap-4 sm:grid-cols-2">
{gallery.map((item) => (
<div
key={item.mediaId}
className="overflow-hidden rounded-2xl bg-paper-deep"
>
<img
src={item.url}
alt=""
className="h-full w-full object-contain"
/>
</div>
))}
</div>
) : null}
{portfolio.mainTextHtml ? (
<div className="reveal reveal-delay-2 mx-auto max-w-3xl">
<HtmlContent html={portfolio.mainTextHtml} />
</div>
) : null}
</div>
</article>
</ContentPageShell>
);
}