Polish product/blog detail pages: gallery, tabs, Persian names, image fixes
- Product detail: interactive image gallery (thumbnails, prev/next,
hover-zoom), tabbed spec/review/comments section matching the site's
standard pattern, variations/options display, and an availability
label instead of duplicating the contact-store message.
- Fix product.images normalization — the API returns {url} media objects,
not plain strings, so galleries were silently rendering empty.
- Prefer the Persian name (nameFa/productNameFa) everywhere a product
name is shown.
- Product cards: padded, rounded images instead of edge-to-edge with a
dashed divider.
- Blog: wire the homepage carousel to real posts (was static sample
data), drop the detail page's separate cover image, and make images
embedded in article/description HTML fill their container with
rounded corners.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
2b5f4e437b
commit
7fb1f5a60f
@@ -1,5 +1,4 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { Placeholder } from "@/components/ui/Placeholder";
|
||||
import { getBlog } from "@/lib/meshkee";
|
||||
|
||||
function formatDate(iso?: string) {
|
||||
@@ -39,16 +38,6 @@ export default async function BlogDetailPage({
|
||||
{post.title}
|
||||
</h1>
|
||||
|
||||
{post.image && (
|
||||
<Placeholder
|
||||
src={post.image}
|
||||
alt={post.title}
|
||||
width="100%"
|
||||
height="360px"
|
||||
className="mb-8 w-full rounded-2xl"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="prose max-w-none"
|
||||
dangerouslySetInnerHTML={{ __html: post.mainTextHtml }}
|
||||
|
||||
@@ -34,13 +34,15 @@ export default async function BlogListPage({
|
||||
href={`/blog/${post.slug}`}
|
||||
className="group overflow-hidden rounded-xl border border-border bg-surface transition-shadow hover:shadow-md"
|
||||
>
|
||||
<Placeholder
|
||||
src={post.image ?? "/images/placeholder.svg"}
|
||||
alt={post.title}
|
||||
width="100%"
|
||||
height="180px"
|
||||
className="w-full rounded-none border-0"
|
||||
/>
|
||||
<div className="p-[10px]">
|
||||
<Placeholder
|
||||
src={post.image ?? "/images/placeholder.svg"}
|
||||
alt={post.title}
|
||||
width="100%"
|
||||
fit="contain"
|
||||
className="aspect-square w-full rounded-lg border-0"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="mb-2 flex items-center gap-2 text-[11px] text-muted">
|
||||
{post.categoryName && <span>{post.categoryName}</span>}
|
||||
|
||||
@@ -83,3 +83,9 @@ body {
|
||||
.carousel-track::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.prose img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
+19
-10
@@ -9,6 +9,7 @@ import { BrandGroupCarousels } from "@/components/home/BrandGroupCarousels";
|
||||
import { AboutSanihome } from "@/components/home/AboutSanihome";
|
||||
import { BlogCarousel } from "@/components/home/BlogCarousel";
|
||||
import {
|
||||
getBlogs,
|
||||
getBusinessInfo,
|
||||
getCategories,
|
||||
getProducts,
|
||||
@@ -20,15 +21,23 @@ import {
|
||||
} from "@/lib/meshkee";
|
||||
|
||||
export default async function Home() {
|
||||
const [categories, storeSpecials, productsResult, brandGroups, staticImageSlots, businessInfo] =
|
||||
await Promise.all([
|
||||
getCategories(),
|
||||
getStoreSpecials(),
|
||||
getProducts({ pageSize: 12 }),
|
||||
getBrandGroups(),
|
||||
getStaticImageSlots(),
|
||||
getBusinessInfo(),
|
||||
]);
|
||||
const [
|
||||
categories,
|
||||
storeSpecials,
|
||||
productsResult,
|
||||
brandGroups,
|
||||
staticImageSlots,
|
||||
businessInfo,
|
||||
blogsResult,
|
||||
] = await Promise.all([
|
||||
getCategories(),
|
||||
getStoreSpecials(),
|
||||
getProducts({ pageSize: 12 }),
|
||||
getBrandGroups(),
|
||||
getStaticImageSlots(),
|
||||
getBusinessInfo(),
|
||||
getBlogs({ pageSize: 8 }),
|
||||
]);
|
||||
|
||||
const contactPhone = businessInfo?.phoneNumbers[0];
|
||||
const specialSaleProducts = storeSpecials[0]?.items ?? [];
|
||||
@@ -55,7 +64,7 @@ export default async function Home() {
|
||||
/>
|
||||
<BrandGroupCarousels brandGroups={brandGroups} contactPhone={contactPhone} />
|
||||
<AboutSanihome />
|
||||
<BlogCarousel />
|
||||
<BlogCarousel posts={blogsResult.items} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { Placeholder } from "@/components/ui/Placeholder";
|
||||
import { ProductGallery } from "@/components/product/ProductGallery";
|
||||
import { ProductTabs } from "@/components/product/ProductTabs";
|
||||
import { formatPrice } from "@/data/sample";
|
||||
import { getBusinessInfo, getProduct, getProductTechnicalInfo } from "@/lib/meshkee";
|
||||
import {
|
||||
getBusinessInfo,
|
||||
getProduct,
|
||||
getProductTechnicalInfo,
|
||||
getProductVariations,
|
||||
} from "@/lib/meshkee";
|
||||
|
||||
export default async function ProductDetailPage({
|
||||
params,
|
||||
@@ -9,9 +15,10 @@ export default async function ProductDetailPage({
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const [product, technicalInfo, businessInfo] = await Promise.all([
|
||||
const [product, technicalInfo, variations, businessInfo] = await Promise.all([
|
||||
getProduct(slug),
|
||||
getProductTechnicalInfo(slug),
|
||||
getProductVariations(slug),
|
||||
getBusinessInfo(),
|
||||
]);
|
||||
const contactPhone = businessInfo?.phoneNumbers[0];
|
||||
@@ -22,30 +29,12 @@ export default async function ProductDetailPage({
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-4 py-10">
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<div className="grid gap-3">
|
||||
<Placeholder
|
||||
src={images[0] ?? "/images/placeholder.svg"}
|
||||
alt={product.name}
|
||||
width="100%"
|
||||
className="aspect-square w-full rounded-2xl"
|
||||
/>
|
||||
{images.length > 1 && (
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
{images.slice(1, 5).map((img) => (
|
||||
<Placeholder
|
||||
key={img}
|
||||
src={img}
|
||||
alt={product.name}
|
||||
width="100%"
|
||||
className="aspect-square rounded-xl"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-8 md:grid-cols-12">
|
||||
<div className="md:col-span-5">
|
||||
<ProductGallery images={images} alt={product.name} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="md:col-span-7">
|
||||
{product.brand && (
|
||||
<p className="font-display mb-2 text-xs tracking-wide text-muted uppercase">
|
||||
{product.brand}
|
||||
@@ -61,37 +50,65 @@ export default async function ProductDetailPage({
|
||||
</a>
|
||||
)}
|
||||
|
||||
<div className="my-5 flex flex-wrap items-center gap-3">
|
||||
{product.minPrice != null ? (
|
||||
<div className="my-5 flex flex-wrap items-center gap-4">
|
||||
{product.minPrice != null && (
|
||||
<span className="text-2xl font-bold text-red-dark">
|
||||
{product.minPrice === product.maxPrice
|
||||
? formatPrice(product.minPrice)
|
||||
: `${formatPrice(product.minPrice)} - ${formatPrice(product.maxPrice ?? product.minPrice)}`}
|
||||
<span className="mr-1 text-sm font-normal text-muted">تومان</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-lg text-muted">تماس با فروشگاه</span>
|
||||
)}
|
||||
<span
|
||||
className={`rounded-full px-3 py-1 text-xs font-medium ${
|
||||
product.inStock ? "bg-green-50 text-green-700" : "bg-red-light text-red-dark"
|
||||
}`}
|
||||
>
|
||||
{product.inStock ? "موجود" : "ناموجود"}
|
||||
<span className="flex items-center gap-2 text-sm">
|
||||
<span className="font-medium text-foreground">موجودی:</span>
|
||||
<span
|
||||
className={`rounded-full px-3 py-1 text-xs font-medium ${
|
||||
product.inStock ? "bg-green-50 text-green-700" : "bg-red-light text-red-dark"
|
||||
}`}
|
||||
>
|
||||
{product.inStock ? "موجود" : "ناموجود"}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{variations.length > 0 && (
|
||||
<div className="mb-6 space-y-4">
|
||||
{variations.map((variation) => (
|
||||
<div key={variation.id}>
|
||||
<p className="mb-2 text-sm font-bold">{variation.name}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{variation.options.map((option) => (
|
||||
<span
|
||||
key={option.id}
|
||||
className="flex items-center gap-1.5 rounded-lg border border-border px-3 py-1.5 text-sm"
|
||||
>
|
||||
{option.colorHex && (
|
||||
<span
|
||||
className="h-3 w-3 shrink-0 rounded-full border border-border"
|
||||
style={{ backgroundColor: option.colorHex }}
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
{option.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{product.minPrice != null ? (
|
||||
<button
|
||||
type="button"
|
||||
className="mb-6 w-full rounded-xl bg-red py-3 text-sm font-medium text-white transition-colors hover:bg-red-dark md:w-auto md:px-10"
|
||||
className="w-full rounded-xl bg-red py-3 text-sm font-medium text-white transition-colors hover:bg-red-dark md:w-auto md:px-10"
|
||||
>
|
||||
افزودن به سبد خرید
|
||||
</button>
|
||||
) : contactPhone ? (
|
||||
<a
|
||||
href={`tel:${contactPhone}`}
|
||||
className="mb-6 flex w-full items-center justify-center gap-2 rounded-xl bg-red py-3 text-sm font-medium text-white transition-colors hover:bg-red-dark md:w-auto md:px-10"
|
||||
className="flex w-full items-center justify-center gap-2 rounded-xl bg-red py-3 text-sm font-medium text-white transition-colors hover:bg-red-dark md:w-auto md:px-10"
|
||||
>
|
||||
<span aria-hidden>📞</span>
|
||||
تماس با فروشگاه
|
||||
@@ -100,36 +117,15 @@ export default async function ProductDetailPage({
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
className="mb-6 w-full cursor-not-allowed rounded-xl bg-red/60 py-3 text-sm font-medium text-white md:w-auto md:px-10"
|
||||
className="w-full cursor-not-allowed rounded-xl bg-red/60 py-3 text-sm font-medium text-white md:w-auto md:px-10"
|
||||
>
|
||||
تماس با فروشگاه
|
||||
</button>
|
||||
)}
|
||||
|
||||
{product.descriptionHtml && (
|
||||
<div
|
||||
className="prose prose-sm max-w-none border-t border-border pt-5"
|
||||
dangerouslySetInnerHTML={{ __html: product.descriptionHtml }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{technicalInfo.length > 0 && (
|
||||
<div className="mt-10 border-t border-border pt-8">
|
||||
<h2 className="mb-4 text-lg font-bold text-red-dark">مشخصات فنی</h2>
|
||||
<table className="w-full overflow-hidden rounded-xl border border-border text-sm">
|
||||
<tbody>
|
||||
{technicalInfo.map((field) => (
|
||||
<tr key={field.label} className="border-b border-border last:border-0 odd:bg-background">
|
||||
<th className="w-1/3 p-3 text-right font-medium text-muted">{field.label}</th>
|
||||
<td className="p-3">{field.value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
<ProductTabs technicalInfo={technicalInfo} descriptionHtml={product.descriptionHtml} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user