Header polish: dynamic logo, action button restyle, nav scope, bug fixes
- Header/footer logo now reads from GET /tenants/{domain}/website/favicon
(logoDarkUrl on the navy bar, logoUrl on the white footer) with a local
fallback, matching the favicon's existing dynamic behavior.
- Mobile header rebuilt as its own 3-column row: hamburger right, logo
centered, cart left. Fixed a missing justify-center that left the
hamburger icon (and its X state) off-center inside its square button.
- Header action buttons (cart, user/login) restyled to solid primary
pills/circles per a reference screenshot; a search button was tried and
then removed since the site has no search backend yet.
- Navbar reverted to exactly 5 static items (home, workshops, blog, about,
contact) per instruction — a categories-driven "محصولات" dropdown was
built and then removed the same session; /products, /store, /portfolios
remain real pages, just unlinked from the header for now.
- Hero: second avatar raised so it's not hidden behind the character.
WhyChooseUs: mirrored the two bottom floating badges into symmetric
corners (were overlapping). FeatureCards: restored the 4th feature
("انتخاب زمان کلاس") that had been silently dropped, gave the icon
badges a nested white-circle treatment, and gave the cards a visible
border.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
8f102a8250
commit
84a1c0f9cc
+13
-2
@@ -15,15 +15,26 @@
|
||||
|
||||
The user asked for the homepage's structure/layout to closely match the live original (`http://ali-alemi.local:8000`, a local Meshkee web-builder preview blocked by their VPN from this environment — they instead pasted screenshots directly). Corrections made after comparing against those screenshots:
|
||||
|
||||
- **Header**: solid `bg-secondary` (navy) bar, not white/blurred. Logo sits first in DOM (renders on the **right** in RTL — do not add `order-*` overrides here again, that was tried and was wrong), nav links, then phone/login actions, matching the real desktop header exactly.
|
||||
- **Header**: solid `bg-secondary` (navy) bar, not white/blurred. On desktop (`lg:` and up) logo sits first in DOM (renders on the **right** in RTL — do not add `order-*` overrides here again, that was tried and was wrong), nav links, then phone/cart/login actions, matching the real desktop header exactly. **Below `lg:` the header is a completely separate 3-column grid row** (hamburger right / logo centered / cart left — Farsi readers expect the hamburger on the reading-start side, not the logo-adjacent corner) rendered via its own `grid grid-cols-3 lg:hidden` block, not responsive classes bolted onto the desktop flex row — the two arrangements are different enough (centered logo vs. edge logo, hamburger only exists on mobile) that trying to share one row fighting `order-*`/`justify-*` was worse than two small dedicated blocks. See `Header.tsx`.
|
||||
- **Hero** (`HeroSection.tsx`): character + a large dotted ring backdrop (`public/images/hero/ring.png` — the accent dots are baked into that asset, don't draw separate circle divs), a second smaller student avatar peeking from behind the character, floating grad-cap/paper-plane/shape decorations. No CTA buttons in the hero — the original doesn't have any there. Two-column grid switches at **`md:`** (768px), not `lg:` — at `lg:` the in-between tablet/small-laptop range stacked to a single column and looked broken (large blank gap), since the image side has fixed-height containers.
|
||||
- **Feature cards** (`FeatureCards.tsx`): a **Swiper carousel** (prev/next arrow buttons, not pagination dots) in a two-column row — cards on the left (wide column), a "ویژگیهای ما" heading + short paragraph + the arrow buttons in a narrow **280px right-hand column**. This grid must be `md:grid-cols-[280px_minmax(0,1fr)]` — **not** `[280px_1fr]`: a bare `1fr` track let Swiper's internal width calculation blow the column out to ~16.7M px (classic CSS Grid blowout; Swiper needs `minmax(0,1fr)` to be constrained by its parent). The Swiper's own grid-item wrapper also needs `min-w-0` directly (see the `Reveal` around it) — that's the part that matters **below** `md`, where there's no explicit `grid-template-columns` at all but the item is still a grid child with `min-width: auto` by default, so the same blowout happened on mobile too until that class was added. Any future Swiper placed inside a `grid` or `flex` container needs this same `min-w-0` treatment regardless of breakpoint. Also must NOT have negative top margin overlapping the hero — that was tried first and the user flagged it as a bug ("باید با هیرو فاصله داشته باشه"), plain `py-16 md:py-24` is correct.
|
||||
- **Quick-links row** ("چرا استاد علی عالمی؟", `QuickLinks.tsx`): real icon assets (`icons/play.png`, `exam.png`, `price.png`, `support.png`) in `sm:grid-cols-2 lg:grid-cols-4` — 4-in-a-row on real desktop widths, 2×2 on tablet. Ends with a "درباره ما بیشتر بدانید" CTA linking `/about-us` (that's the original frame's own link field, not invented).
|
||||
- **"چرا مارا انتخاب کنید؟"** (`WhyChooseUs.tsx`): image on the **left**, text+CTA on the **right** (DOM order text-first renders right in RTL grid). Stat badges (آموزش با کیفیت / نظارت دقیق و ارزیابی عملکرد / بیش از ۱۵۰ ساعت آموزش / ۱۴,۵۰۰ نفر دانشجو+avatars) are **floating cards scattered over the image's corners** (absolute-positioned with physical `left-*`/`right-*`, not logical `start`/`end` — this composition doesn't flip with page direction), not a plain bulleted list in the text column.
|
||||
- **"چرا مارا انتخاب کنید؟"** (`WhyChooseUs.tsx`): image on the **left**, text+CTA on the **right** (DOM order text-first renders right in RTL grid). Stat badges (آموزش با کیفیت / نظارت دقیق و ارزیابی عملکرد / بیش از ۱۵۰ ساعت آموزش / ۱۴,۵۰۰ نفر دانشجو+avatars) are **floating cards scattered over the image's corners** (absolute-positioned with physical `left-*`/`right-*`, not logical `start`/`end` — this composition doesn't flip with page direction), not a plain bulleted list in the text column. The two **bottom** badges must sit in mirrored corners — `۱۴,۵۰۰ نفر دانشجو` at `-bottom-6 -left-6` and `بیش از ۱۵۰ ساعت آموزش` at `-bottom-6 -right-6` — an earlier `right-1/3` on the second one drifted under the first at narrower widths (user flagged it as an overlap bug from a screenshot); symmetric corners is the fix, don't reintroduce a fractional/centered offset on either bottom badge.
|
||||
- **Testimonials**: multi-per-view on desktop (`breakpoints: {768:2, 1024:3}`), pagination dots (not arrows) — opposite of the feature carousel.
|
||||
- **Stats bar** (`StatsBar.tsx`): light/white background with **teal** numbers — not a navy full-bleed banner (that was tried first and was wrong).
|
||||
- Scroll-reveal entrance animation on every section via `src/components/Reveal.tsx` (fade + translateY on `IntersectionObserver`, stands in for the original's WOW.js + animate.css) — requested explicitly ("انیمیشنها ... باید دقیقا شبیه سایت اصلی باشه").
|
||||
- **General lesson**: any two-column `image + text` grid using a fixed-height image container should switch at `md:` not `lg:`, or verify the `lg:` in-between range doesn't leave a tall single-column stack. Applies to `WhyChooseUs.tsx`, `about/BioIntro.tsx`, `contact/ContactInfo.tsx` (already fixed) — check any *new* two-column section against this before shipping.
|
||||
- **Hero second avatar** (`avatar-1.png`, the girl peeking from behind the character): positioned `top-16 end-0` (was `bottom-6 end-2`, which put almost the whole thing behind the character's body with only a sliver of hair visible) — user asked to raise it so it actually reads as a second figure. If the character illustration or its box size changes, re-check this is still clearly visible, not just technically in the DOM.
|
||||
- **Feature cards** (`FeatureCards.tsx`) use `border border-ink/15 shadow-md`, not `ring-1 ring-line` — the `--color-line` token (`#ececec`) was too light to read as a border at all against the white card/white section background (flagged from a screenshot as "بوردرهاشون مشخص نیست"). If another white-on-white card ever looks edge-less, this is the fix, not a heavier shadow alone.
|
||||
|
||||
## Header — logo/favicon, nav, categories (round 2 polish)
|
||||
|
||||
- **Dynamic logo**: `RootLayout` (`src/app/layout.tsx`) now fetches `getFavicon()` once and passes `logoUrl`/`logoDarkUrl` down to both `Header` and `Footer` (in addition to the favicon it already used for `<head>` icons). `Header` prefers `logoDarkUrl` (the dashboard's dark-background variant) → `logoUrl` → the bundled local `/images/logo.png`; the `brightness-0 invert` CSS filter is applied **only** when falling back to the local asset (it's the one known-teal file that needs forcing white for the navy bar — a real CMS-uploaded logo's color is unknown, don't invert it blind). `Footer` just uses `logoUrl` → local, no invert (white background, original color is correct there). Both use `unoptimized` on `next/image` when the src is a remote CMS URL, since that host isn't (and can't be, until a real tenant exists) in `next.config.ts` `images.remotePatterns`. The favicon itself was already dynamic before this round (`generateMetadata`) — this was specifically about the header/footer `<img>` logo not following the same rule yet.
|
||||
- **Active nav link + phone button styling**: active link and the phone number both render as a solid white pill (`rounded-full bg-white px-4 py-2`, dark `text-secondary`) against the navy bar, not a transparent/tinted-text treatment — this was an explicit visual ask from a screenshot annotation ("دور لینک اکتیو ... یک مستطیل سفید با ردیوس ۵۰ بذار"). `rounded-full` (Tailwind's 9999px) reads as the same fully-pill shape as a literal 50px radius at this button height, so that's the correct utility, not an arbitrary `rounded-[50px]`. Inactive links stay `text-white/80` on transparent. Applies only to the desktop nav row; the mobile dropdown panel keeps its own simpler hover-background treatment.
|
||||
- **Navbar reverted to exactly 5 static items** (`NAV_LINKS` in `src/lib/site.ts`): صفحه اصلی، دورههای آموزشی، مقالات، درباره ما، تماس با ما — per explicit instruction, **not** محصولات/فروشگاه/نمونهکارها. A categories-driven dropdown for "محصولات" (`src/components/CategoriesMenu.tsx`, fetched via `getCategories({ entityType: "product" })` in the root layout) was built and then **removed again** the same session once the user clarified the nav should stay to just those 5 until real CMS content exists — `CategoriesMenu.tsx` was deleted and the `categories` fetch/prop taken back out of `layout.tsx`/`Header.tsx`. `/products`, `/store`, `/portfolios` are still real, working pages — just not linked from the header nav right now. If nav-driven category browsing is wanted again later, it needs to be rebuilt (check git history for the deleted `CategoriesMenu.tsx` rather than starting from scratch).
|
||||
- **Header action buttons** (search/cart/user, right side of the desktop row): redesigned to solid `bg-primary` filled circles/pills per a reference screenshot, not outlined/ghost buttons. **Search was added then removed the same session** — the site has no search backend, so don't re-add a search icon/box to the header without an actual `/products?name=` wiring decision first. `CartButton` (`src/components/cart/CartButton.tsx`) is now always a solid primary circle with a white-on-primary badge (no more `light` prop — it was only ever used inside this navy header, the conditional was dead weight). `UserMenu` (`src/components/UserMenu.tsx`) similarly dropped its `light` prop — both the logged-out "ورود / ثبتنام" pill and the logged-in "{name} عزیز" trigger are solid primary pills now.
|
||||
- **Mobile hamburger button bug**: the button was `flex items-center` but **not** `justify-center`, so the 3-line icon span (and the X it morphs into when open) hugged one edge of the 40×40 square instead of sitting centered — reported from a screenshot ("۳ خط منو داخل مربع نیستن" / X not centered when open). Fix was adding `justify-center` alongside the existing `justify-self-start` (that one's unrelated — it positions the *button itself* within the mobile header's 3-column grid row; `justify-center` centers the icon *inside* the button). If another icon-only square button ever looks off-center, check for this exact same missing class rather than touching the icon's own SVG/span markup.
|
||||
- **Feature carousel must have all 4 real items**: `src/components/home/features-data.ts` had been trimmed to 3 during the earlier "match the real 3-visible-at-once desktop screenshot" pass, silently dropping "انتخاب زمان کلاس" (choose class time) — the original theme has 4 features, the 4th just wasn't visible in that one screenshot because the carousel only shows 3 at a time on desktop. Restored as the 2nd item (clock icon, secondary tone) so the carousel actually has something to scroll to. Icon badges in `FeatureCards.tsx` also got a nested treatment — an outer `h-16 w-16 rounded-2xl` tone-colored square containing an inner `h-10 w-10 rounded-full bg-white` circle with the icon colored to match the outer tone — per a "flaticon-style" reference screenshot; a flat single-layer colored-square-with-white-icon read as low-effort by comparison. `about/FeatureStrip.tsx` reuses the same `FEATURES` array and benefits from the same fix (was showing an awkward 3-in-a-4-col grid before).
|
||||
|
||||
## Scope (current)
|
||||
|
||||
|
||||
+3
-3
@@ -46,7 +46,7 @@ export default async function RootLayout({
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const session = await getSession();
|
||||
const [session, brand] = await Promise.all([getSession(), safe(getFavicon(), NO_BRAND_ASSETS)]);
|
||||
const user = session?.user ?? null;
|
||||
const isAdmin = isAdminUser(user);
|
||||
|
||||
@@ -55,9 +55,9 @@ export default async function RootLayout({
|
||||
<body className="flex min-h-screen flex-col antialiased">
|
||||
<ToastProvider>
|
||||
<CartProvider>
|
||||
<Header user={user} isAdmin={isAdmin} />
|
||||
<Header user={user} isAdmin={isAdmin} logoUrl={brand.logoUrl ?? null} logoDarkUrl={brand.logoDarkUrl ?? null} />
|
||||
<main className="flex-1">{children}</main>
|
||||
<Footer />
|
||||
<Footer logoUrl={brand.logoUrl ?? null} />
|
||||
<MiniCart />
|
||||
</CartProvider>
|
||||
</ToastProvider>
|
||||
|
||||
@@ -18,12 +18,20 @@ const SOCIALS = [
|
||||
{ icon: IconTelegram, href: CONTACT.telegram, label: "تلگرام" },
|
||||
];
|
||||
|
||||
export function Footer() {
|
||||
export function Footer({ logoUrl }: { logoUrl?: string | null } = {}) {
|
||||
const footerLogoSrc = logoUrl || "/images/logo.png";
|
||||
return (
|
||||
<footer className="border-t border-line bg-white">
|
||||
<div className="boxed grid gap-12 py-16 text-center lg:grid-cols-[1.3fr_1fr_1fr] lg:text-right">
|
||||
<div className="mx-auto lg:mx-0">
|
||||
<Image src="/images/logo.png" alt={SITE_NAME} width={150} height={44} className="mx-auto h-10 w-auto lg:mx-0" />
|
||||
<Image
|
||||
src={footerLogoSrc}
|
||||
alt={SITE_NAME}
|
||||
width={150}
|
||||
height={44}
|
||||
unoptimized={Boolean(logoUrl)}
|
||||
className="mx-auto h-10 w-auto lg:mx-0"
|
||||
/>
|
||||
<ul className="mt-6 space-y-2 text-sm leading-7 text-body">
|
||||
{BIO_LINES.map((line) => (
|
||||
<li key={line}>{line}</li>
|
||||
|
||||
+74
-47
@@ -10,73 +10,99 @@ import { CartButton } from "@/components/cart/CartButton";
|
||||
import { UserMenu } from "@/components/UserMenu";
|
||||
import { IconPhone } from "./icons";
|
||||
|
||||
export function Header({ user, isAdmin }: { user: CustomerUser | null; isAdmin: boolean }) {
|
||||
const ACTIVE_LINK = "rounded-full bg-white px-4 py-2 text-sm font-semibold text-secondary";
|
||||
const INACTIVE_LINK = "whitespace-nowrap rounded-full px-4 py-2 text-sm font-medium text-white/80 transition-colors hover:text-primary";
|
||||
|
||||
export function Header({
|
||||
user,
|
||||
isAdmin,
|
||||
logoUrl,
|
||||
logoDarkUrl,
|
||||
}: {
|
||||
user: CustomerUser | null;
|
||||
isAdmin: boolean;
|
||||
logoUrl: string | null;
|
||||
logoDarkUrl: string | null;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// Header sits on the navy bar (dark background): prefer the dashboard's
|
||||
// dark-background logo variant, then its light variant, then the bundled
|
||||
// local mark — only the local fallback needs the CSS invert (see
|
||||
// CONTEXT.md "Dynamic logo/favicon").
|
||||
const headerLogoSrc = logoDarkUrl || logoUrl || "/images/logo.png";
|
||||
const headerLogoNeedsInvert = !logoDarkUrl && !logoUrl;
|
||||
|
||||
const Logo = (
|
||||
<Link href="/" className="shrink-0" onClick={() => setOpen(false)}>
|
||||
<Image
|
||||
src={headerLogoSrc}
|
||||
alt="استاد علی عالمی"
|
||||
width={140}
|
||||
height={40}
|
||||
priority
|
||||
unoptimized={Boolean(logoDarkUrl || logoUrl)}
|
||||
className={`h-9 w-auto ${headerLogoNeedsInvert ? "brightness-0 invert" : ""}`}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 bg-secondary">
|
||||
<div className="boxed flex h-20 items-center justify-between gap-4">
|
||||
<Link href="/" className="shrink-0" onClick={() => setOpen(false)}>
|
||||
<Image
|
||||
src="/images/logo.png"
|
||||
alt="استاد علی عالمی"
|
||||
width={140}
|
||||
height={40}
|
||||
priority
|
||||
className="h-9 w-auto brightness-0 invert"
|
||||
/>
|
||||
</Link>
|
||||
{/* Mobile/tablet row: hamburger on the right, logo centered, cart on the left. */}
|
||||
<div className="boxed grid h-20 grid-cols-3 items-center lg:hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
aria-label="باز کردن منو"
|
||||
aria-expanded={open}
|
||||
className="flex h-10 w-10 items-center justify-center justify-self-start rounded-lg border border-white/20"
|
||||
>
|
||||
<span className="relative block h-3.5 w-5">
|
||||
<span
|
||||
className={`absolute inset-x-0 top-0 h-0.5 rounded bg-white transition-transform ${open ? "translate-y-[6px] rotate-45" : ""}`}
|
||||
/>
|
||||
<span className={`absolute inset-x-0 top-1/2 h-0.5 -translate-y-1/2 rounded bg-white transition-opacity ${open ? "opacity-0" : ""}`} />
|
||||
<span
|
||||
className={`absolute inset-x-0 bottom-0 h-0.5 rounded bg-white transition-transform ${open ? "-translate-y-[6px] -rotate-45" : ""}`}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<nav className="hidden items-center gap-5 xl:flex">
|
||||
<div className="justify-self-center">{Logo}</div>
|
||||
|
||||
<div className="justify-self-end">
|
||||
<CartButton />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop row: logo right, nav middle, phone/user/cart left (standard RTL order). */}
|
||||
<div className="boxed hidden h-20 items-center justify-between gap-4 lg:flex">
|
||||
{Logo}
|
||||
|
||||
<nav className="flex items-center gap-1">
|
||||
{NAV_LINKS.map((link) => {
|
||||
const active = link.href === "/" ? pathname === "/" : pathname.startsWith(link.href);
|
||||
return (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={`whitespace-nowrap text-sm font-medium transition-colors hover:text-primary ${
|
||||
active ? "text-primary" : "text-white/80"
|
||||
}`}
|
||||
>
|
||||
<Link key={link.href} href={link.href} className={active ? ACTIVE_LINK : INACTIVE_LINK}>
|
||||
{link.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="hidden items-center gap-3 lg:flex">
|
||||
<div className="flex items-center gap-3">
|
||||
<a
|
||||
href={`tel:${CONTACT.phone}`}
|
||||
className="flex items-center gap-2 text-sm font-medium text-white/80 transition-colors hover:text-primary"
|
||||
className="flex items-center gap-2 rounded-full bg-white px-4 py-2 text-sm font-medium text-secondary"
|
||||
dir="ltr"
|
||||
>
|
||||
<IconPhone className="h-4 w-4 text-primary" />
|
||||
{CONTACT.phoneLabel}
|
||||
</a>
|
||||
<CartButton light />
|
||||
<UserMenu user={user} isAdmin={isAdmin} light />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 lg:hidden">
|
||||
<CartButton light />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
aria-label="باز کردن منو"
|
||||
aria-expanded={open}
|
||||
className="flex h-10 w-10 items-center justify-center rounded-lg border border-white/20"
|
||||
>
|
||||
<span className="relative block h-3.5 w-5">
|
||||
<span
|
||||
className={`absolute inset-x-0 top-0 h-0.5 rounded bg-white transition-transform ${open ? "translate-y-[6px] rotate-45" : ""}`}
|
||||
/>
|
||||
<span className={`absolute inset-x-0 top-1/2 h-0.5 -translate-y-1/2 rounded bg-white transition-opacity ${open ? "opacity-0" : ""}`} />
|
||||
<span
|
||||
className={`absolute inset-x-0 bottom-0 h-0.5 rounded bg-white transition-transform ${open ? "-translate-y-[6px] -rotate-45" : ""}`}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
<UserMenu user={user} isAdmin={isAdmin} />
|
||||
<CartButton />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -93,13 +119,14 @@ export function Header({ user, isAdmin }: { user: CustomerUser | null; isAdmin:
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<div className="mt-2 flex flex-col gap-3 border-t border-white/10 pt-4">
|
||||
<a href={`tel:${CONTACT.phone}`} className="flex items-center gap-2 px-3 text-sm text-white/80" dir="ltr">
|
||||
<a href={`tel:${CONTACT.phone}`} className="mx-3 flex items-center gap-2 rounded-full bg-white px-4 py-2 text-sm font-medium text-secondary" dir="ltr">
|
||||
<IconPhone className="h-4 w-4 text-primary" />
|
||||
{CONTACT.phoneLabel}
|
||||
</a>
|
||||
<div className="px-3">
|
||||
<UserMenu user={user} isAdmin={isAdmin} light />
|
||||
<UserMenu user={user} isAdmin={isAdmin} />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -5,14 +5,14 @@ import { BUSINESS_URL, CUSTOMER_URL, customerLoginUrl } from "@/lib/site";
|
||||
import type { CustomerUser } from "@/lib/meshkee/types";
|
||||
import { IconChevronDown, IconUser } from "@/components/icons";
|
||||
|
||||
export function UserMenu({ user, isAdmin, light = false }: { user: CustomerUser | null; isAdmin: boolean; light?: boolean }) {
|
||||
export function UserMenu({ user, isAdmin }: { user: CustomerUser | null; isAdmin: boolean }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<a
|
||||
href={customerLoginUrl()}
|
||||
className="flex items-center gap-2 rounded-full bg-primary px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-primary-dark"
|
||||
className="flex items-center gap-2 whitespace-nowrap rounded-full bg-primary px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-primary-dark"
|
||||
>
|
||||
<IconUser className="h-4 w-4" />
|
||||
ورود / ثبتنام
|
||||
@@ -27,11 +27,9 @@ export function UserMenu({ user, isAdmin, light = false }: { user: CustomerUser
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className={`flex items-center gap-2 rounded-full px-4 py-2.5 text-sm font-semibold transition-colors ${
|
||||
light ? "text-white/90 hover:bg-white/10" : "text-ink-soft hover:bg-surface"
|
||||
}`}
|
||||
className="flex items-center gap-2 whitespace-nowrap rounded-full bg-primary px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-primary-dark"
|
||||
>
|
||||
<IconUser className="h-4 w-4 text-primary" />
|
||||
<IconUser className="h-4 w-4" />
|
||||
{displayName}
|
||||
<IconChevronDown className={`h-3.5 w-3.5 transition-transform ${open ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useCart } from "@/components/providers/CartProvider";
|
||||
import { IconCart } from "@/components/icons";
|
||||
|
||||
export function CartButton({ light = false }: { light?: boolean }) {
|
||||
export function CartButton() {
|
||||
const { totalQuantity, open } = useCart();
|
||||
|
||||
return (
|
||||
@@ -11,13 +11,11 @@ export function CartButton({ light = false }: { light?: boolean }) {
|
||||
type="button"
|
||||
onClick={open}
|
||||
aria-label="سبد خرید"
|
||||
className={`relative flex h-10 w-10 items-center justify-center rounded-full transition-colors ${
|
||||
light ? "text-white/90 hover:bg-white/10" : "text-ink-soft hover:bg-surface"
|
||||
}`}
|
||||
className="relative flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-primary text-white transition-colors hover:bg-primary-dark"
|
||||
>
|
||||
<IconCart className="h-5 w-5" />
|
||||
<IconCart className="h-4.5 w-4.5" />
|
||||
{totalQuantity > 0 && (
|
||||
<span className="absolute -top-1 -end-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-bold text-white">
|
||||
<span className="absolute -top-1 -end-1 flex h-5 min-w-5 items-center justify-center rounded-full border-2 border-secondary bg-white px-1 text-[10px] font-bold text-primary">
|
||||
{totalQuantity.toLocaleString("fa-IR")}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -55,13 +55,15 @@ export function FeatureCards() {
|
||||
>
|
||||
{FEATURES.map(({ icon: Icon, title, text, tone }) => (
|
||||
<SwiperSlide key={title}>
|
||||
<div className="flex h-full flex-col rounded-2xl bg-white p-7 shadow-sm ring-1 ring-line">
|
||||
<div className="flex h-full flex-col rounded-2xl border border-ink/15 bg-white p-7 shadow-md">
|
||||
<span
|
||||
className={`flex h-16 w-16 items-center justify-center rounded-2xl text-white ${
|
||||
className={`flex h-16 w-16 items-center justify-center rounded-2xl ${
|
||||
tone === "primary" ? "bg-primary" : "bg-secondary"
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-7 w-7" />
|
||||
<span className="flex h-10 w-10 items-center justify-center rounded-full bg-white">
|
||||
<Icon className={`h-5 w-5 ${tone === "primary" ? "text-primary" : "text-secondary"}`} />
|
||||
</span>
|
||||
</span>
|
||||
<h3 className="mt-6 text-lg font-bold text-ink">{title}</h3>
|
||||
<p className="mt-3 text-sm leading-6 text-body">{text}</p>
|
||||
|
||||
@@ -61,14 +61,14 @@ export function HeroSection() {
|
||||
className="absolute top-1/2 -start-3 h-12 w-12 animate-bounce [animation-duration:3s] md:h-14 md:w-14"
|
||||
/>
|
||||
|
||||
{/* Second student peeking from behind the main character. */}
|
||||
{/* Second student peeking from behind the main character — raised so it isn't hidden behind the body. */}
|
||||
<Image
|
||||
src="/images/hero/avatar-1.png"
|
||||
alt=""
|
||||
width={140}
|
||||
height={140}
|
||||
aria-hidden
|
||||
className="absolute bottom-6 end-2 z-[5] h-24 w-24 md:h-28 md:w-28"
|
||||
className="absolute top-16 end-0 z-[5] h-20 w-20 md:top-20 md:h-24 md:w-24"
|
||||
/>
|
||||
|
||||
<Image
|
||||
|
||||
@@ -51,7 +51,7 @@ export function WhyChooseUs() {
|
||||
<span className="text-xs font-bold text-ink-soft">نظارت دقیق و ارزیابی عملکرد</span>
|
||||
</div>
|
||||
|
||||
<div className="absolute -bottom-5 right-1/3 flex items-center gap-2 rounded-2xl bg-white px-4 py-3 shadow-xl ring-1 ring-line">
|
||||
<div className="absolute -bottom-6 -right-6 flex items-center gap-2 rounded-2xl bg-white px-4 py-3 shadow-xl ring-1 ring-line">
|
||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-secondary text-white">
|
||||
<IconPencil className="h-4 w-4" />
|
||||
</span>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IconHome, IconUser, IconWifi } from "@/components/icons";
|
||||
import { IconClock, IconHome, IconUser, IconWifi } from "@/components/icons";
|
||||
|
||||
export const FEATURES = [
|
||||
{
|
||||
@@ -8,8 +8,8 @@ export const FEATURES = [
|
||||
tone: "primary" as const,
|
||||
},
|
||||
{
|
||||
icon: IconUser,
|
||||
title: "بستههای معلم خصوصی",
|
||||
icon: IconClock,
|
||||
title: "انتخاب زمان کلاس",
|
||||
text: "لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است",
|
||||
tone: "secondary" as const,
|
||||
},
|
||||
@@ -19,4 +19,10 @@ export const FEATURES = [
|
||||
text: "لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است",
|
||||
tone: "primary" as const,
|
||||
},
|
||||
{
|
||||
icon: IconUser,
|
||||
title: "بستههای معلم خصوصی",
|
||||
text: "لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است",
|
||||
tone: "secondary" as const,
|
||||
},
|
||||
];
|
||||
|
||||
+4
-3
@@ -37,12 +37,13 @@ export const CONTACT = {
|
||||
telegram: "https://t.me/Alialemiteacher",
|
||||
};
|
||||
|
||||
// Kept to exactly these 5 per explicit instruction: /products, /store,
|
||||
// /portfolios stay real, reachable pages (linked from the homepage etc.)
|
||||
// but are deliberately left out of the header nav until their content is
|
||||
// actually coming from the CMS — see CONTEXT.md "Navbar scope".
|
||||
export const NAV_LINKS = [
|
||||
{ href: "/", label: "صفحه اصلی" },
|
||||
{ href: "/workshops", label: "دوره های آموزشی" },
|
||||
{ href: "/products", label: "محصولات" },
|
||||
{ href: "/store", label: "فروشگاه" },
|
||||
{ href: "/portfolios", label: "نمونه کارها" },
|
||||
{ href: "/blog", label: "مقالات" },
|
||||
{ href: "/about-us", label: "درباره ما" },
|
||||
{ href: "/contact-us", label: "تماس با ما" },
|
||||
|
||||
Reference in New Issue
Block a user