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:
co-authored by
Cursor
parent
112e5eb46d
commit
4e635bb038
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user