Ship local assets, portfolio filters, favicon, and UI polish.

Host CDN images locally, add top-level portfolio category filtering with nav sublinks, refresh the Meshkee favicon, and tighten title/gallery/footer presentation across the site.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-07-31 09:30:35 +03:30
co-authored by Cursor
parent 112e5eb46d
commit 4e635bb038
230 changed files with 955 additions and 174 deletions
+68 -33
View File
@@ -3,10 +3,20 @@ import Link from "next/link";
import { notFound } from "next/navigation";
import { ContentPageShell } from "@/components/ContentPageShell";
import { HtmlContent } from "@/components/HtmlContent";
import { PortfolioGallery } from "@/components/PortfolioGallery";
import {
categoryLabelFa,
findPortfolioCategory,
getRootCategories,
portfolioCategoryHref,
topPortfolioCategories,
type PortfolioCategory,
} from "@/data/portfolioCategories";
import {
ApiNotFoundError,
formatFaDate,
getPortfolio,
getPortfolioCategories,
getPortfolioCoverImage,
} from "@/lib/api";
@@ -14,6 +24,36 @@ type PortfolioDetailPageProps = {
params: Promise<{ slug: string }>;
};
function mapApiCategories(
items: Awaited<ReturnType<typeof getPortfolioCategories>>,
): PortfolioCategory[] {
return items.map((item) => ({
id: String(item.id),
slug: item.slug,
nameEn: item.name,
nameFa: item.nameFa || item.name,
parentId: item.parentId,
sortOrder: item.sortOrder ?? 0,
}));
}
function findRootForCategory(
categoryId: string | number | null | undefined,
categories: PortfolioCategory[],
): PortfolioCategory | undefined {
if (categoryId == null) return undefined;
let current = categories.find((c) => c.id === String(categoryId));
if (!current) return undefined;
while (current?.parentId) {
const parent = categories.find((c) => c.id === String(current?.parentId));
if (!parent) break;
current = parent;
}
return current;
}
export async function generateMetadata({
params,
}: PortfolioDetailPageProps): Promise<Metadata> {
@@ -47,6 +87,18 @@ export default async function PortfolioDetailPage({
const cover = getPortfolioCoverImage(portfolio);
const date = formatFaDate(portfolio.publishedAt);
const title = portfolio.titleFa || portfolio.title;
let allCategories: PortfolioCategory[] = topPortfolioCategories;
try {
allCategories = mapApiCategories(await getPortfolioCategories());
} catch {
allCategories = topPortfolioCategories;
}
const categoryFa = categoryLabelFa(portfolio.categoryName, allCategories);
const rootCategory =
findRootForCategory(portfolio.categoryId, allCategories) ??
findPortfolioCategory(portfolio.categoryName, getRootCategories(allCategories));
const gallery = (portfolio.gallery ?? []).filter(
(item) => item.url !== cover,
);
@@ -57,16 +109,18 @@ export default async function PortfolioDetailPage({
<div className="mx-auto max-w-5xl px-5 lg:px-8">
<div className="reveal mb-10 max-w-3xl">
<Link
href="/portfolio"
href={
rootCategory
? portfolioCategoryHref(rootCategory.slug)
: "/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>
{categoryFa ? (
<p className="mb-3 text-[11px] text-muted">{categoryFa}</p>
) : null}
<h1 className="whitespace-normal text-[clamp(1.35rem,3.8vw,2.5rem)] font-normal leading-tight tracking-tight text-ink">
@@ -74,13 +128,13 @@ export default async function PortfolioDetailPage({
</h1>
{portfolio.titleEn ? (
<p className="latin mt-3 text-xs uppercase tracking-widest text-muted">
<p className="latin title-en text-muted">
{portfolio.titleEn}
</p>
) : null}
{portfolio.abstract ? (
<p className="mt-5 text-sm leading-8 text-muted md:text-base">
<p className="mt-5 text-justify text-sm leading-8 text-muted md:text-base">
{portfolio.abstract}
</p>
) : null}
@@ -95,32 +149,13 @@ export default async function PortfolioDetailPage({
) : 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}
<PortfolioGallery
cover={cover}
images={gallery.map((item) => ({
id: item.mediaId,
url: item.url,
}))}
/>
{portfolio.mainTextHtml ? (
<div className="reveal reveal-delay-2 mx-auto max-w-3xl">