Compare commits

...
7 Commits
Author SHA1 Message Date
amirhosein.ashourlooandClaude Sonnet 5 69c54d3b17 Set footer top border color to #dddddd
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 19:41:15 +03:30
amirhosein.ashourlooandClaude Sonnet 5 c4411ebc28 Set mobile/non-desktop header logo height to 65px
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 19:40:17 +03:30
amirhosein.ashourlooandClaude Sonnet 5 69d17a6f8f Change footer background to #f1f1f1
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 19:39:04 +03:30
amirhosein.ashourlooandClaude Sonnet 5 d292bfc74c Resize header login/cart icon buttons to 40px, cart bg to #f1f1f1
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 19:35:15 +03:30
amirhosein.ashourlooandClaude Sonnet 5 d090f57540 Redesign header icons/account-cart chip, light footer, fix sticky-footer gap
- New shared SVG icon set (src/components/ui/icons.tsx): Menu, User,
  Cart, Close, ChevronLeft — replaces emoji (☰ 👤 🛒 ✕) across the
  header, and the literal "←" text arrow on every "view more"-style
  link (SectionTitle's action, the mega-menu link, BlogCarousel's
  "ادامه مطلب").
- Header desktop account/cart area redesigned as a borderless chip:
  cart icon button (right) + divider + account icon-badge/two-line
  "حساب کاربری / ورود/ثبت‌نام" (left), matching a supplied reference.
  Got the RTL DOM order backwards on the first pass (cart needs to be
  first in DOM to render rightmost) and corrected it.
- Header logo: -10px (was already doubled from an earlier request).
- Footer: background is now bg-[#9a21291a] (exact hex given, not a
  token substitute) with black text throughout instead of solid
  dark-red/white; footer logo doubled; the meshkee-credit logo gets
  brightness-0 so the white source asset stays visible on the new
  light background.
- Fixed a real layout bug: <main> had no flex-1, so on any page
  shorter than the viewport the footer stuck to the content instead of
  the bottom of the screen, leaving a gap of bare background beneath
  it. Standard sticky-footer flex fix.
- CONTEXT.md: documented the icon set, the account/cart chip's RTL DOM
  ordering (to stop this regressing again), and the footer's new
  color scheme + the flex-1 fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 19:31:00 +03:30
amirhosein.ashourlooandClaude Sonnet 5 7a78adc7f3 Footer Meshkee credit widget, fix carousel title row alignment/action button
- Footer bottom-right credit link now matches the badge used on other
  Meshkee storefronts (reference: safeteb.com) — logo + two-line label
  inside an animated corner-drawing border on hover, linking to
  https://meshkee.com. Added the shared credit-widget logo asset
  (public/images/meshkee-logo-white.png).
- SectionTitle: title/action row uses items-center instead of items-end
  (was visibly offset once a carousel title could wrap or carry a
  caption); the action button ("مشاهده همه" etc.) is now styled like
  the site's other primary buttons (filled red, white text, rounded)
  instead of a plain text link.
- Carousel English captions (SPECIAL SALE, TOP SELLING, FAVORITE SELL)
  moved back from a below-title "enSubtitle" line to the existing
  inline enTitle treatment, matching every other section heading's
  convention on the site.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 19:14:10 +03:30
amirhosein.ashourlooandClaude Sonnet 5 ab1b07ff28 Footer credit line links to meshkee.com
Replace the static "Designed for Sanihome" text with a real link
("Powered by meshkee.com" -> https://meshkee.com).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 18:58:56 +03:30
10 changed files with 246 additions and 74 deletions
+76 -8
View File
@@ -180,6 +180,28 @@ persistent memory as "for all the user's sites", not just this one)
in the array — first DOM child renders rightmost under `dir="rtl"`). in the array — first DOM child renders rightmost under `dir="rtl"`).
- Category tile images: 110px, no border, no border-radius - Category tile images: 110px, no border, no border-radius
(`FeaturedCategories.tsx`). (`FeaturedCategories.tsx`).
- **No emoji as icons** — `src/components/ui/icons.tsx` has the shared
light/outline SVG set (`MenuIcon`, `UserIcon`, `CartIcon`, `CloseIcon`,
`ChevronLeftIcon`), 24x24 viewBox, `stroke="currentColor"`. Use these
instead of characters like "☰"/"👤"/"🛒"/"✕". Every "view more"-style
link/button (`SectionTitle`'s `action`, the mega-menu's "مشاهده همه",
`BlogCarousel`'s "ادامه مطلب") uses `ChevronLeftIcon`, not a literal "←"
character — keep new ones consistent with that.
- **Header account/cart cluster** (`Header.tsx`, desktop only —
`hidden ... sm:flex`): a borderless `bg-surface` chip holding, in this
exact DOM order, `CartButton`, a divider, then the login link. That DOM
order is deliberate and easy to get backwards: under `dir="rtl"` the
first DOM child renders rightmost, so cart-first-in-DOM is what actually
puts the cart icon on the visual **right** and the login block on the
**left** (this was built backwards once and corrected). Same rule inside
the login link itself — the icon badge comes before the two-line text in
DOM so the icon renders right of the text. Neither `CartButton` nor this
wrapper has a border (removed on request — don't reintroduce one).
`CartButton` is a fixed 44px (`h-11 w-11`) icon-only square everywhere it
renders (also stands alone, un-wrapped, below `sm`); its unread-count
badge sits at `-top-1.5 -right-1.5`, not the product-card cart badge's
`-left-1.5` — don't copy that positioning between the two, they're
intentionally different corners for different-looking buttons.
## Static-image slots ## Static-image slots
@@ -222,14 +244,17 @@ time the page renders, with zero code changes.
`SectionTitle`'s `enTitle` prop is optional for exactly this reason — these `SectionTitle`'s `enTitle` prop is optional for exactly this reason — these
dynamic groups only have one `title` string from the API, not a Persian/ dynamic groups only have one `title` string from the API, not a Persian/
English pair, so the inline English-caps subtitle just doesn't render for English pair. Each carousel still shows an English caption using that same
them. Instead, each carousel shows an English caption *under* the title `enTitle` slot (inline, beside the title — the same treatment as every
(`SectionTitle`'s separate `enSubtitle` prop, `opacity-50`, its own line) — other section heading on the site, e.g. BlogCarousel's "آخرین مطالب وبلاگ /
`StoreSpecialCarousels.englishLabelFromKey()` formats the group's `key` LATEST BLOG"; there was a brief detour through a separate "below the title,
(e.g. `top-selling` → "TOP SELLING") rather than inventing a translation of opacity-50, own line" `enSubtitle` prop, which the user then asked to match
`title`; there's no real localized-English field to translate from, and a the existing inline convention instead — don't reintroduce that below-title
made-up translation would be fabricated data. Skips rendering if the key variant). `StoreSpecialCarousels.englishLabelFromKey()` formats the group's
has no letters at all (fell back to a bare numeric id). `key` (e.g. `top-selling` → "TOP SELLING") rather than inventing a
translation of `title`; there's no real localized-English field to
translate from, and a made-up translation would be fabricated data. Skips
rendering if the key has no letters at all (fell back to a bare numeric id).
**Banner interleaving**: the two static-image banner sections (`TwoBanners`, **Banner interleaving**: the two static-image banner sections (`TwoBanners`,
`ThreeBanners`) are rendered *inside* `StoreSpecialCarousels`, one inserted `ThreeBanners`) are rendered *inside* `StoreSpecialCarousels`, one inserted
@@ -238,6 +263,14 @@ cycling through the two if there end up being more than two carousels. Don't
move them back out to `page.tsx` as fixed-position siblings — that only move them back out to `page.tsx` as fixed-position siblings — that only
works for a known, fixed carousel count, which this deliberately isn't. works for a known, fixed carousel count, which this deliberately isn't.
`SectionTitle`'s title/action row uses `items-center` (not `items-end`) —
needed once carousel titles could carry more content (the inline `enTitle`
caption) than a single line, so bottom-aligning the row left the action
button visibly offset from the title block. The `action` button itself is
styled like the site's other primary buttons (filled `bg-red`, white text,
`rounded-lg` — the same look as `ProductCard`'s "افزودن به سبد"), not a
plain text link.
**"مشاهده همه" (view all)**: each carousel's action button links to **"مشاهده همه" (view all)**: each carousel's action button links to
`/products?specialId={group.id}`. `store-specials` groups are admin-curated, `/products?specialId={group.id}`. `store-specials` groups are admin-curated,
already-complete lists (the endpoint has no pagination) — there's no already-complete lists (the endpoint has no pagination) — there's no
@@ -263,6 +296,41 @@ render a `<link rel="icon">` and browsers aren't guaranteed to prefer the
second one, so keeping both was silently serving the stale static icon. second one, so keeping both was silently serving the stale static icon.
Don't add `app/favicon.ico` (or `public/favicon.ico`) back. Don't add `app/favicon.ico` (or `public/favicon.ico`) back.
## Footer: light background, black text, sticky to viewport bottom
The footer is `bg-[#f1f1f1]` (exact hex given by the user, same neutral
gray as `CartButton`'s background — not swapped for a design-token color;
an earlier version used a red-tinted `#9a21291a`, since replaced) with
black text throughout (`text-black`, `/70`, `/50`, `/10`, `/20` opacity
steps) — it used to be a solid dark-red block with white text; don't revert
to that. The footer logo (`logoDarkUrl ?? logoUrl`) is `h-[72px]`, doubled
from its original size on request. Because `public/images/meshkee-logo-
white.png` (the credit-widget logo below) is a solid-white asset, it gets
`brightness-0` on this light background to render as a black silhouette
instead of invisible white-on-near-white — the same `opacity-60 →
group-hover:opacity-100` fade still applies on top of that.
`<main>` has `flex-1` in `layout.tsx` (`body` is `flex flex-col`) so the
footer sits flush at the bottom of the viewport on short pages instead of
leaving a gap of bare `background` color beneath it — this is the standard
sticky-footer flex pattern. Don't remove `flex-1` from `main` without
re-checking a short page (e.g. `/contact`) for that gap coming back.
## Footer: Meshkee credit widget
The footer's bottom-right link ("Designed And Developed By Meshkee
E-Commerce Team" → `https://meshkee.com`) intentionally matches the credit
badge Meshkee's own storefronts use elsewhere (reference: safeteb.com) —
same markup shape (frame + logo + two-line label), same animated corner
border (four 1px lines that draw in on hover, staggered L→B→R→T, and
un-draw in reverse on mouse-leave via matching `transition-delay`s on the
resting state). `public/images/meshkee-logo-white.png` is that reference
site's own logo asset, pulled down and committed here since it's the
platform's shared credit-widget logo, not something unique to safeteb.com.
If this ever needs to change, re-derive the exact hover-timing values from
a live Meshkee site rather than guessing new ones — the staggered reverse
un-draw is the distinctive part, not just "a border that appears."
## Route map ## Route map
| Route | Notes | | Route | Notes |
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+1 -1
View File
@@ -53,7 +53,7 @@ export default async function RootLayout({ children }: LayoutProps<"/">) {
<Providers> <Providers>
<TopBar /> <TopBar />
<Header categoryTree={categoryTree} categories={categories} logoUrl={businessInfo?.logoUrl} /> <Header categoryTree={categoryTree} categories={categories} logoUrl={businessInfo?.logoUrl} />
<main>{children}</main> <main className="flex-1">{children}</main>
<Footer businessInfo={businessInfo} categories={categories} /> <Footer businessInfo={businessInfo} categories={categories} />
</Providers> </Providers>
</body> </body>
+5 -4
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import { useCart } from "@/context/CartContext"; import { useCart } from "@/context/CartContext";
import { CartIcon } from "@/components/ui/icons";
export function CartButton() { export function CartButton() {
const { itemCount, openPopup } = useCart(); const { itemCount, openPopup } = useCart();
@@ -9,12 +10,12 @@ export function CartButton() {
<button <button
type="button" type="button"
onClick={openPopup} onClick={openPopup}
className="relative flex items-center gap-1.5 rounded-lg border border-border px-3 py-2 transition-colors hover:border-red hover:text-red" aria-label="سبد خرید"
className="relative flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[#f1f1f1] text-foreground transition-colors hover:text-red"
> >
<span aria-hidden>🛒</span> <CartIcon className="h-5 w-5" />
<span className="hidden sm:inline">سبد خرید</span>
{itemCount > 0 && ( {itemCount > 0 && (
<span className="absolute -top-1.5 -left-1.5 flex h-5 min-w-5 items-center justify-center rounded-full bg-red px-1 text-[10px] font-bold text-white"> <span className="absolute -top-1.5 -right-1.5 flex h-5 min-w-5 items-center justify-center rounded-full bg-red px-1 text-[10px] font-bold text-white">
{itemCount} {itemCount}
</span> </span>
)} )}
+4 -2
View File
@@ -2,6 +2,7 @@ import { Placeholder } from "@/components/ui/Placeholder";
import { SectionTitle } from "@/components/ui/SectionTitle"; import { SectionTitle } from "@/components/ui/SectionTitle";
import { Carousel } from "@/components/ui/Carousel"; import { Carousel } from "@/components/ui/Carousel";
import { EmptyState } from "@/components/ui/EmptyState"; import { EmptyState } from "@/components/ui/EmptyState";
import { ChevronLeftIcon } from "@/components/ui/icons";
import type { BlogSummary } from "@/lib/meshkee"; import type { BlogSummary } from "@/lib/meshkee";
function formatDate(iso?: string) { function formatDate(iso?: string) {
@@ -50,8 +51,9 @@ export function BlogCarousel({ posts }: { posts: BlogSummary[] }) {
{post.abstract} {post.abstract}
</p> </p>
)} )}
<span className="mt-3 block text-xs font-medium text-red"> <span className="mt-3 flex items-center gap-1 text-xs font-medium text-red">
ادامه مطلب ← ادامه مطلب
<ChevronLeftIcon className="h-3.5 w-3.5" />
</span> </span>
</div> </div>
</a> </a>
+40 -15
View File
@@ -27,7 +27,7 @@ export function Footer({
); );
return ( return (
<footer className="border-t border-red-dark/20 bg-red-dark text-white"> <footer className="border-t border-[#dddddd] bg-[#f1f1f1] text-black">
<div className="mx-auto max-w-7xl px-4 py-12"> <div className="mx-auto max-w-7xl px-4 py-12">
<div className="grid gap-8 md:grid-cols-4"> <div className="grid gap-8 md:grid-cols-4">
<div> <div>
@@ -37,7 +37,7 @@ export function Footer({
<img <img
src={businessInfo.logoDarkUrl ?? businessInfo.logoUrl ?? undefined} src={businessInfo.logoDarkUrl ?? businessInfo.logoUrl ?? undefined}
alt={businessInfo.nameFa || "سانی‌هوم"} alt={businessInfo.nameFa || "سانی‌هوم"}
className="h-9 w-auto object-contain" className="h-[72px] w-auto object-contain"
/> />
) : ( ) : (
<span className="font-display text-lg font-bold tracking-tight"> <span className="font-display text-lg font-bold tracking-tight">
@@ -46,16 +46,16 @@ export function Footer({
)} )}
</div> </div>
{businessInfo?.about && ( {businessInfo?.about && (
<p className="text-sm leading-relaxed text-white/70">{businessInfo.about}</p> <p className="text-sm leading-relaxed text-black/70">{businessInfo.about}</p>
)} )}
</div> </div>
<div> <div>
<h4 className="mb-3 text-sm font-bold">دسترسی سریع</h4> <h4 className="mb-3 text-sm font-bold">دسترسی سریع</h4>
<ul className="space-y-2 text-sm text-white/70"> <ul className="space-y-2 text-sm text-black/70">
{quickLinks.map((link) => ( {quickLinks.map((link) => (
<li key={link}> <li key={link}>
<a href="#" className="transition-colors hover:text-white"> <a href="#" className="transition-colors hover:text-black">
{link} {link}
</a> </a>
</li> </li>
@@ -66,12 +66,12 @@ export function Footer({
<div> <div>
<h4 className="mb-3 text-sm font-bold">دسته‌بندی‌ها</h4> <h4 className="mb-3 text-sm font-bold">دسته‌بندی‌ها</h4>
{categories.length === 0 ? ( {categories.length === 0 ? (
<p className="text-sm text-white/50">هنوز دسته‌بندی‌ای ثبت نشده است.</p> <p className="text-sm text-black/50">هنوز دسته‌بندی‌ای ثبت نشده است.</p>
) : ( ) : (
<ul className="space-y-2 text-sm text-white/70"> <ul className="space-y-2 text-sm text-black/70">
{categories.slice(0, 5).map((cat) => ( {categories.slice(0, 5).map((cat) => (
<li key={cat.id}> <li key={cat.id}>
<a href="#" className="transition-colors hover:text-white"> <a href="#" className="transition-colors hover:text-black">
{cat.name} {cat.name}
</a> </a>
</li> </li>
@@ -83,7 +83,7 @@ export function Footer({
<div> <div>
<h4 className="mb-3 text-sm font-bold">تماس با ما</h4> <h4 className="mb-3 text-sm font-bold">تماس با ما</h4>
{hasContactInfo ? ( {hasContactInfo ? (
<ul className="space-y-2 text-sm text-white/70"> <ul className="space-y-2 text-sm text-black/70">
{businessInfo?.addresses.map((address) => <li key={address}>{address}</li>)} {businessInfo?.addresses.map((address) => <li key={address}>{address}</li>)}
{businessInfo?.phoneNumbers.map((phone) => ( {businessInfo?.phoneNumbers.map((phone) => (
<li key={phone}>تلفن: {phone}</li> <li key={phone}>تلفن: {phone}</li>
@@ -91,7 +91,7 @@ export function Footer({
{businessInfo?.emails.map((email) => <li key={email}>ایمیل: {email}</li>)} {businessInfo?.emails.map((email) => <li key={email}>ایمیل: {email}</li>)}
</ul> </ul>
) : ( ) : (
<p className="text-sm text-white/50">هنوز اطلاعات تماس ثبت نشده است.</p> <p className="text-sm text-black/50">هنوز اطلاعات تماس ثبت نشده است.</p>
)} )}
{socialEntries.length > 0 && ( {socialEntries.length > 0 && (
<div className="mt-4 flex gap-3"> <div className="mt-4 flex gap-3">
@@ -101,7 +101,7 @@ export function Footer({
href={url} href={url}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="font-display flex h-8 w-8 items-center justify-center rounded-full border border-white/20 text-[10px] font-bold transition-colors hover:border-white hover:bg-white/10" className="font-display flex h-8 w-8 items-center justify-center rounded-full border border-black/20 text-[10px] font-bold transition-colors hover:border-black hover:bg-black/5"
> >
{socialLabels[key] ?? key.slice(0, 2).toUpperCase()} {socialLabels[key] ?? key.slice(0, 2).toUpperCase()}
</a> </a>
@@ -111,11 +111,36 @@ export function Footer({
</div> </div>
</div> </div>
<div className="mt-10 flex flex-col items-center justify-between gap-3 border-t border-white/10 pt-6 text-xs text-white/50 md:flex-row"> <div className="mt-10 flex flex-col items-center justify-between gap-3 border-t border-black/10 pt-6 text-xs text-black/50 md:flex-row">
<p>© ۱۴۰۵ سانی‌هوم — تمامی حقوق محفوظ است.</p> <p>© ۱۴۰۵ سانی‌هوم — تمامی حقوق محفوظ است.</p>
<p className="font-display tracking-wide uppercase"> <a
Designed for Sanihome href="https://meshkee.com"
</p> target="_blank"
rel="noopener noreferrer"
dir="ltr"
className="group relative inline-flex items-center gap-2.5 px-2.5 py-1.5 text-black/50 transition-colors duration-300 hover:text-black"
>
<span className="pointer-events-none absolute inset-0" aria-hidden="true">
<span className="absolute top-0 left-0 h-full w-px origin-top scale-y-0 bg-black/40 transition-transform duration-[280ms] delay-[420ms] group-hover:scale-y-100 group-hover:delay-0" />
<span className="absolute bottom-0 left-0 h-px w-full origin-left scale-x-0 bg-black/40 transition-transform duration-[280ms] delay-[280ms] group-hover:scale-x-100 group-hover:delay-[140ms]" />
<span className="absolute top-0 right-0 h-full w-px origin-bottom scale-y-0 bg-black/40 transition-transform duration-[280ms] delay-[140ms] group-hover:scale-y-100 group-hover:delay-[280ms]" />
<span className="absolute top-0 left-0 h-px w-full origin-right scale-x-0 bg-black/40 transition-transform duration-[280ms] delay-0 group-hover:scale-x-100 group-hover:delay-[420ms]" />
</span>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/images/meshkee-logo-white.png"
alt="Meshkee"
width={42}
height={42}
className="relative block h-[42px] w-auto shrink-0 object-contain opacity-60 brightness-0 transition-opacity duration-300 group-hover:opacity-100"
/>
<span className="relative flex min-h-[42px] flex-col justify-center leading-[1.25]">
<span className="text-[11px]">Designed And Developed By</span>
<span className="text-[13px] font-semibold text-black/70 transition-colors duration-300 group-hover:text-black">
Meshkee E-Commerce Team
</span>
</span>
</a>
</div> </div>
</div> </div>
</footer> </footer>
+33 -18
View File
@@ -5,6 +5,7 @@ import { createPortal } from "react-dom";
import type { Category, CategoryNode, CategoryTreeGroup } from "@/lib/meshkee"; import type { Category, CategoryNode, CategoryTreeGroup } from "@/lib/meshkee";
import { EmptyState } from "@/components/ui/EmptyState"; import { EmptyState } from "@/components/ui/EmptyState";
import { CartButton } from "@/components/cart/CartButton"; import { CartButton } from "@/components/cart/CartButton";
import { ChevronLeftIcon, CloseIcon, MenuIcon, UserIcon } from "@/components/ui/icons";
import { SearchBar } from "./SearchBar"; import { SearchBar } from "./SearchBar";
const navLinks = [ const navLinks = [
@@ -26,8 +27,8 @@ function Logo({ src, large = false }: { src?: string | null; large?: boolean })
alt="سانی‌هوم" alt="سانی‌هوم"
className={ className={
large large
? "h-[42px] w-auto object-contain md:h-[46px]" ? "h-[65px] w-auto object-contain"
: "h-20 w-auto object-contain md:h-24" : "h-[70px] w-auto object-contain md:h-[86px]"
} }
/> />
</a> </a>
@@ -107,9 +108,9 @@ function MobileDrawer({
type="button" type="button"
onClick={onClose} onClick={onClose}
aria-label="بستن منو" aria-label="بستن منو"
className="flex h-9 w-9 items-center justify-center rounded-lg text-xl text-muted transition-colors hover:bg-red-light hover:text-red" className="flex h-9 w-9 items-center justify-center rounded-lg text-muted transition-colors hover:bg-red-light hover:text-red"
> >
✕ <CloseIcon className="h-5 w-5" />
</button> </button>
</div> </div>
@@ -188,7 +189,7 @@ function MobileDrawer({
href={LOGIN_URL} href={LOGIN_URL}
className="flex items-center gap-2 px-5 py-4 text-right font-medium transition-colors hover:bg-red-light/40 hover:text-red" className="flex items-center gap-2 px-5 py-4 text-right font-medium transition-colors hover:bg-red-light/40 hover:text-red"
> >
<span aria-hidden>👤</span> <UserIcon className="h-4 w-4" />
ورود ورود
</a> </a>
</nav> </nav>
@@ -231,9 +232,9 @@ export function Header({
type="button" type="button"
onClick={() => setDrawerOpen(true)} onClick={() => setDrawerOpen(true)}
aria-label="منو" aria-label="منو"
className="flex h-10 w-10 items-center justify-center rounded-lg border border-border text-xl transition-colors hover:border-red hover:text-red min-[1200px]:hidden" className="flex h-10 w-10 items-center justify-center rounded-lg border border-border transition-colors hover:border-red hover:text-red min-[1200px]:hidden"
> >
<span aria-hidden>☰</span> <MenuIcon className="h-5 w-5" />
</button> </button>
<div className="hidden min-[1200px]:block"> <div className="hidden min-[1200px]:block">
<Logo src={logoUrl} /> <Logo src={logoUrl} />
@@ -250,14 +251,27 @@ export function Header({
</div> </div>
<div className="flex items-center justify-end gap-3 text-sm"> <div className="flex items-center justify-end gap-3 text-sm">
<a <div className="hidden items-center gap-2 rounded-2xl bg-surface p-1.5 sm:flex">
href={LOGIN_URL} <CartButton />
className="hidden items-center gap-1.5 text-muted transition-colors hover:text-red sm:flex" <span className="h-8 w-px bg-border" aria-hidden />
> <a
<span>👤</span> href={LOGIN_URL}
<span>ورود</span> className="flex items-center gap-2 rounded-xl py-1 pr-1 transition-opacity hover:opacity-80"
</a> >
<CartButton /> <span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-red text-white">
<UserIcon className="h-5 w-5" />
</span>
<span className="text-right leading-tight">
<span className="block text-sm font-bold whitespace-nowrap text-foreground">
حساب کاربری
</span>
<span className="block text-xs whitespace-nowrap text-muted">ورود/ثبت‌نام</span>
</span>
</a>
</div>
<div className="sm:hidden">
<CartButton />
</div>
</div> </div>
</div> </div>
@@ -279,7 +293,7 @@ export function Header({
aria-haspopup="true" aria-haspopup="true"
className="flex shrink-0 items-center gap-2 rounded-lg bg-red px-4 py-2 font-medium text-white transition-colors hover:bg-red-dark" className="flex shrink-0 items-center gap-2 rounded-lg bg-red px-4 py-2 font-medium text-white transition-colors hover:bg-red-dark"
> >
<span aria-hidden>☰</span> <MenuIcon className="h-4 w-4" />
دسته‌بندی‌ها دسته‌بندی‌ها
</button> </button>
@@ -294,9 +308,10 @@ export function Header({
</div> </div>
<a <a
href="/products" href="/products"
className="text-xs font-medium text-red hover:text-red-dark" className="flex items-center gap-1 text-xs font-medium text-red hover:text-red-dark"
> >
مشاهده همه ← مشاهده همه
<ChevronLeftIcon className="h-3.5 w-3.5" />
</a> </a>
</div> </div>
@@ -11,8 +11,9 @@ import { productHref, type ProductGroup, type StaticImageWithUrl } from "@/lib/m
// localized English field to pull a real translation from, and inventing // localized English field to pull a real translation from, and inventing
// one would be fabricated data. `key` is real data and usually a readable // one would be fabricated data. `key` is real data and usually a readable
// English slug (e.g. "top-selling"), so it's what gets formatted into the // English slug (e.g. "top-selling"), so it's what gets formatted into the
// English caption under the title. Skip it if the key has no letters at // English caption beside the title (same inline treatment as every other
// all (e.g. it fell back to a bare numeric id). // section heading on the site, e.g. BlogCarousel's "LATEST BLOG"). Skip it
// if the key has no letters at all (e.g. it fell back to a bare numeric id).
function englishLabelFromKey(key: string): string | undefined { function englishLabelFromKey(key: string): string | undefined {
const label = key.replace(/[-_]+/g, " ").trim(); const label = key.replace(/[-_]+/g, " ").trim();
return /[a-zA-Z]/.test(label) ? label.toUpperCase() : undefined; return /[a-zA-Z]/.test(label) ? label.toUpperCase() : undefined;
@@ -58,7 +59,7 @@ export function StoreSpecialCarousels({
<div className="mx-auto max-w-7xl px-4"> <div className="mx-auto max-w-7xl px-4">
<SectionTitle <SectionTitle
title={group.title} title={group.title}
enSubtitle={englishLabelFromKey(group.key)} enTitle={englishLabelFromKey(group.key)}
action="مشاهده همه" action="مشاهده همه"
actionHref={`/products?specialId=${group.id}`} actionHref={`/products?specialId=${group.id}`}
/> />
+7 -23
View File
@@ -1,24 +1,16 @@
import { ChevronLeftIcon } from "@/components/ui/icons";
type SectionTitleProps = { type SectionTitleProps = {
title: string; title: string;
enTitle?: string; enTitle?: string;
// Shown on its own line under the title (not inline like enTitle), at 50%
// opacity — used for the homepage's dynamic store-specials carousels.
enSubtitle?: string;
subtitle?: string; subtitle?: string;
action?: string; action?: string;
actionHref?: string; actionHref?: string;
}; };
export function SectionTitle({ export function SectionTitle({ title, enTitle, subtitle, action, actionHref }: SectionTitleProps) {
title,
enTitle,
enSubtitle,
subtitle,
action,
actionHref,
}: SectionTitleProps) {
return ( return (
<div className="mb-6 flex items-end justify-between gap-4"> <div className="mb-6 flex items-center justify-between gap-4">
<div> <div>
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1"> <div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
<h2 className="text-xl font-bold text-red-dark md:text-2xl">{title}</h2> <h2 className="text-xl font-bold text-red-dark md:text-2xl">{title}</h2>
@@ -32,15 +24,6 @@ export function SectionTitle({
</span> </span>
)} )}
</div> </div>
{enSubtitle && (
<p
className="font-display mt-1 text-xs font-semibold tracking-wide text-foreground uppercase opacity-50 md:text-sm"
lang="en"
dir="ltr"
>
{enSubtitle}
</p>
)}
{subtitle && ( {subtitle && (
<p className="mt-1 text-sm text-muted">{subtitle}</p> <p className="mt-1 text-sm text-muted">{subtitle}</p>
)} )}
@@ -48,9 +31,10 @@ export function SectionTitle({
{action && actionHref && ( {action && actionHref && (
<a <a
href={actionHref} href={actionHref}
className="shrink-0 text-sm font-medium text-red transition-colors hover:text-red-dark" className="flex shrink-0 items-center gap-1.5 rounded-lg bg-red px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-red-dark"
> >
{action} ← {action}
<ChevronLeftIcon className="h-4 w-4" />
</a> </a>
)} )}
</div> </div>
+76
View File
@@ -0,0 +1,76 @@
type IconProps = { className?: string };
// Shared light/outline icon set — thin stroke (1.75), rounded caps/joins,
// 24x24 viewBox, matching the chevrons already used in Carousel/ProductGallery.
// Prefer these over emoji anywhere in the UI.
export function MenuIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" className={className} aria-hidden>
<path
d="M4 7h16M4 12h16M4 17h16"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export function UserIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" className={className} aria-hidden>
<circle cx="12" cy="8" r="3.25" stroke="currentColor" strokeWidth="1.75" />
<path
d="M4.75 19.25c0-3.45 3.24-6.25 7.25-6.25s7.25 2.8 7.25 6.25"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
/>
</svg>
);
}
export function CartIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" className={className} aria-hidden>
<path
d="M3.5 4.5h1.6l1.1 2M6.2 6.5l1.65 7.3a1.5 1.5 0 0 0 1.46 1.17h6.9a1.5 1.5 0 0 0 1.46-1.15l1.23-5.32H6.2Z"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
/>
<circle cx="10" cy="19.5" r="1.15" stroke="currentColor" strokeWidth="1.6" />
<circle cx="16.5" cy="19.5" r="1.15" stroke="currentColor" strokeWidth="1.6" />
</svg>
);
}
export function CloseIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" className={className} aria-hidden>
<path
d="M6 6l12 12M18 6 6 18"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
/>
</svg>
);
}
export function ChevronLeftIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="none" className={className} aria-hidden>
<path
d="M14.5 6.5 9 12l5.5 5.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}