Show real titleFa captions, 3x logo size, fix category hover border

- CategoryCarousel: caption under each circle now comes from the slot
  image's own titleFa (as the business typed it) instead of the resolved
  category's API name - identity resolution via linkUrl slug is still used
  for the href, just not for the visible label
- Replace the hover ring (box-shadow) with a real 1px border - the ring
  rendered oddly against Swiper's transformed slide track, looking like it
  "leaked" into the row above; a border participates in normal box layout
  and doesn't have that problem (border-box sizing means zero layout shift)
- Header/footer logo: h-9 (36px) -> h-[108px], 3x on request; header row
  height bumped to fit. Note: the currently-uploaded (wrong-business) logo
  has a lot of baked-in padding, so it still reads small even at 3x - that's
  the asset, not the sizing
- Re-verified the static-image-slot wiring end to end against the original
  spec (url->img, linkUrl->href, aspectRatio from API, placeholder on
  empty/fail) - all still holds

Also add a Deployment section to AGENTS.md: this repo's whole point is
CI/CD from origin/main, and local commits sitting unpushed (as 3 commits
were, until now) look indistinguishable from unfixed bugs to whoever is
testing the deployed site.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
amirhosein.ashourloo
2026-09-06 07:53:20 +03:30
co-authored by Claude Sonnet 5
parent 2249a07dee
commit 0ffbd953b3
4 changed files with 84 additions and 33 deletions
+30 -4
View File
@@ -58,6 +58,10 @@ Fonts: IRANYekan (fa, local `public/fonts`) + Montserrat (latin, next/font).
mark, not موبایل معلم) — the wiring is correct and will pick up the right mark, not موبایل معلم) — the wiring is correct and will pick up the right
asset automatically the moment it's replaced in the dashboard; don't asset automatically the moment it's replaced in the dashboard; don't
"fix" this by hardcoding the local fallback again. "fix" this by hardcoding the local fallback again.
- Header/footer logo renders at `h-[108px]` (3x the original `h-9`/36px, on
request). If the wrong logo above still looks small at that size, it's
because that specific PNG has a lot of baked-in transparent padding around
the mark — `object-contain` can't fix that, only the replacement asset can.
- Contact data (phone, mobile, branch addresses, Instagram/Telegram) in - Contact data (phone, mobile, branch addresses, Instagram/Telegram) in
`src/data/site.ts` is real, pulled from the live theme's footer — `GET `src/data/site.ts` is real, pulled from the live theme's footer — `GET
/website/business-info` still returns empty `phoneNumbers`/`addresses`/ /website/business-info` still returns empty `phoneNumbers`/`addresses`/
@@ -142,10 +146,21 @@ business's upload order lines up with this site's category sort order. It
doesn't, and there's no contract that it ever would — live data showed a doesn't, and there's no contract that it ever would — live data showed a
laptop photo (`linkUrl` correctly said `?category=laptop`) landing on laptop photo (`linkUrl` correctly said `?category=laptop`) landing on
"موبایل" and "لوازم برقی" appearing with no photo uploaded for it at all, "موبایل" and "لوازم برقی" appearing with no photo uploaded for it at all,
purely because of where it fell in the array. `titleFa` isn't reliable purely because of where it fell in the array. The visible caption under each
either — the same laptop image was titled "تبلت" (stale/copy-pasted text), circle is that image's own `titleFa`, taken as typed — even on the one
contradicting its own `linkUrl`. The `linkUrl` query param was the one field sample where it disagreed with the photo (that same laptop image was titled
that matched the actual photo content every time it was checked. "تبلت"), it's still the business's own editorial text for the tile, and the
`href` (built from the `linkUrl` slug, resolved to this site's own canonical
`/products/category/{id}/{slug}` rather than linking off-site to the literal
`linkUrl`) is what actually has to be correct for navigation to work.
**Category circle hover:** use a real `border` (1px, transparent → `border-brand`
on `group-hover`), not a `ring`/box-shadow. A ring rendered inside a Swiper
slide painted like it was "leaking" into the row above — box-shadow doesn't
participate in normal layout the way a border does, and interacts oddly with
Swiper's transformed `.swiper-wrapper` track. `border` with this site's
global `box-sizing: border-box` reset adds zero layout shift, so it's the
safer default for any future hover-outline effect inside a carousel.
Homepage banner placement (mirrors the logilook.com reference given for this Homepage banner placement (mirrors the logilook.com reference given for this
layout): hero → categories → hot deals → **three-banner-bg** → موبایل layout): hero → categories → hot deals → **three-banner-bg** → موبایل
@@ -222,3 +237,14 @@ npm run dev # localhost:3000
npm run build npm run build
node scripts/gen-category-placeholders.mjs # refresh category thumb placeholders node scripts/gen-category-placeholders.mjs # refresh category thumb placeholders
``` ```
## Deployment
`origin` is `gitea.meshkee.com/meshkee-websites/moalem-shop.git`, `main` branch.
Whatever serves the business's preview/live URL deploys from `origin/main` —
**committing locally is not enough**, push too. A whole session's worth of
work (2026-09-06: about/contact/installments pages, the category-slot
identity fix, live logo/favicon wiring) once sat 3 commits ahead of
`origin/main` unpushed, and the business reported bugs that were already
fixed locally — they were looking at the old deployed code. Check
`git status` for "ahead of origin" before telling anyone a fix is live.
+31 -21
View File
@@ -12,10 +12,11 @@ import type { CategoryNode, StaticImageSlot } from "@/lib/types";
// Fully slot-driven: each image in the "categories" slot carries its own // Fully slot-driven: each image in the "categories" slot carries its own
// linkUrl (e.g. "...?category=mobile"), whose `category` param is the real // linkUrl (e.g. "...?category=mobile"), whose `category` param is the real
// DB slug — that's what identifies which category a photo is for, not the // DB slug — that's what identifies which category a photo is FOR (used to
// image's position in the list or its (business-typed, sometimes wrong) // build the real, in-site link target). The visible caption is the
// titleFa. A category the business hasn't uploaded a photo for simply isn't // business's own titleFa for that image, taken as typed — a category the
// shown here — no placeholder, per the "only what's in the panel" brief. // business hasn't uploaded a photo for simply isn't shown here, no
// placeholder, per the "only what's in the panel" brief.
// See AGENTS.md "Static-image slots" for the mismatch this replaced. // See AGENTS.md "Static-image slots" for the mismatch this replaced.
export default function CategoryCarousel({ export default function CategoryCarousel({
roots, roots,
@@ -29,9 +30,10 @@ export default function CategoryCarousel({
const catSlug = categorySlugFromLinkUrl(img.linkUrl); const catSlug = categorySlugFromLinkUrl(img.linkUrl);
const cat = catSlug ? findNodeBySlug(roots, catSlug) : undefined; const cat = catSlug ? findNodeBySlug(roots, catSlug) : undefined;
if (!cat) return null; if (!cat) return null;
return { key: img.id, art: img.url, cat }; const label = img.titleFa || img.titleEn || cat.nameFa || cat.name;
return { key: img.id, art: img.url, label, href: categoryHref(cat.id, cat.nameFa, cat.name) };
}) })
.filter((x): x is { key: string; art: string; cat: CategoryNode } => Boolean(x)); .filter((x): x is { key: string; art: string; label: string; href: string } => Boolean(x));
if (items.length === 0) return null; if (items.length === 0) return null;
@@ -49,21 +51,29 @@ export default function CategoryCarousel({
1024: { slidesPerView: 7 }, 1024: { slidesPerView: 7 },
}} }}
> >
{items.map(({ key, art, cat }) => { {items.map(({ key, art, label, href }) => (
const label = cat.nameFa || cat.name; <SwiperSlide key={key}>
return ( <Link href={href} className="group flex flex-col items-center gap-2">
<SwiperSlide key={key}> {/* A real border, not ring/box-shadow — box-shadow rings can
<Link href={categoryHref(cat.id, cat.nameFa, cat.name)} className="group flex flex-col items-center gap-2"> paint oddly against Swiper's transformed slide track
<span className="relative size-[104px] overflow-hidden rounded-full bg-surface transition group-hover:bg-brand-soft sm:size-[124px] lg:size-[140px]"> (looked like the border "leaking" into the row above).
<Image src={art} alt={label} fill sizes="140px" className="object-contain p-3" /> border-box sizing (this site's global reset) means the
</span> 1px border adds no layout shift. */}
<span className="text-center text-[13.5px] text-ink transition group-hover:text-brand"> <span className="relative size-[104px] overflow-hidden rounded-full border border-transparent bg-surface transition-all duration-300 group-hover:border-brand group-hover:shadow-md sm:size-[124px] lg:size-[140px]">
{label} <Image
</span> src={art}
</Link> alt={label}
</SwiperSlide> fill
); sizes="140px"
})} className="rounded-full object-contain p-3 transition-transform duration-300 group-hover:scale-110"
/>
</span>
<span className="text-center text-[13.5px] text-ink transition group-hover:text-brand">
{label}
</span>
</Link>
</SwiperSlide>
))}
</Swiper> </Swiper>
</section> </section>
); );
+9 -3
View File
@@ -17,9 +17,15 @@ export default function Footer({
<footer className="mt-14 border-t border-line bg-surface"> <footer className="mt-14 border-t border-line bg-surface">
<div className="boxed grid gap-8 py-10 md:grid-cols-4"> <div className="boxed grid gap-8 py-10 md:grid-cols-4">
<div className="md:col-span-2"> <div className="md:col-span-2">
{/* h-9 w-auto so a square dashboard logo and the wide fallback {/* h-[108px] w-auto so a square dashboard logo and the wide
wordmark both render undistorted — see Header.tsx. */} fallback wordmark both render undistorted — see Header.tsx. */}
<Image src={logoUrl} alt={SITE_NAME} width={140} height={40} className="h-9 w-auto object-contain" /> <Image
src={logoUrl}
alt={SITE_NAME}
width={280}
height={108}
className="h-[108px] w-auto object-contain"
/>
<p className="mt-3 max-w-md text-[13px] leading-7 text-body">{SITE_TAGLINE}</p> <p className="mt-3 max-w-md text-[13px] leading-7 text-body">{SITE_TAGLINE}</p>
<a <a
href={PHONE_HREF} href={PHONE_HREF}
+14 -5
View File
@@ -28,12 +28,21 @@ export default function Header({
<header className="sticky top-0 z-40 w-full border-b border-line bg-white"> <header className="sticky top-0 z-40 w-full border-b border-line bg-white">
<div className="boxed"> <div className="boxed">
{/* Mobile keeps the header down to the search box alone — account, {/* Mobile keeps the header down to the search box alone — account,
cart and browsing live in the fixed bottom bar instead. */} cart and browsing live in the fixed bottom bar instead. The row
<div className="flex h-[70px] items-center gap-4"> is taller on lg so a prominent 108px logo (3x the old 36px) has
room without cramming the search box/buttons beside it. */}
<div className="flex h-[70px] items-center gap-4 lg:h-32">
<Link href="/" className="hidden shrink-0 lg:block" aria-label={SITE_NAME}> <Link href="/" className="hidden shrink-0 lg:block" aria-label={SITE_NAME}>
{/* h-9 w-auto (not a fixed box) so a square dashboard logo mark {/* h-[108px] w-auto (not a fixed box) so a square dashboard logo
and the wide fallback wordmark both render undistorted. */} mark and the wide fallback wordmark both render undistorted. */}
<Image src={logoUrl} alt={SITE_NAME} width={140} height={40} className="h-9 w-auto object-contain" priority /> <Image
src={logoUrl}
alt={SITE_NAME}
width={280}
height={108}
className="h-[108px] w-auto object-contain"
priority
/>
</Link> </Link>
<div className="flex-1"> <div className="flex-1">