feat(landing): glass header nav with side drawer + blog section

- Header: full glassmorphism navigation (services, pricing, estimator, learn,
  blog). Desktop shows an inline glass pill; mobile opens a side drawer that
  slides in from the inline-start edge (right in RTL, left in LTR) over a
  blurred backdrop.
- Nav links smooth-scroll via the shared Lenis instance (exposed through
  scroll-store: setLenis/scrollToId), with an 80px offset under the header.
- "Learn" links to the existing how-it-works steps (id="learn"); add a
  lightweight Blog teaser section (id="blog", "coming soon" cards).
- i18n: fa/en strings for nav and blog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
keyhan
2026-06-23 11:36:27 +03:30
parent 985a23751e
commit f89c3de826
7 changed files with 266 additions and 18 deletions
@@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from 'react';
import dynamic from 'next/dynamic';
import Lenis from 'lenis';
import { setScroll, setPointer, flashState } from './scroll-store';
import { setScroll, setPointer, flashState, setLenis } from './scroll-store';
import { SiteHeader } from './sections/SiteHeader';
import { Hero } from './sections/Hero';
import { ServicesBar } from './sections/ServicesBar';
@@ -14,6 +14,7 @@ import { HowItWorks } from './sections/HowItWorks';
import { Pricing } from './sections/Pricing';
import { Estimator } from './sections/Estimator';
import { Trust } from './sections/Trust';
import { Blog } from './sections/Blog';
import { FinalCta } from './sections/FinalCta';
import { Footer } from './sections/Footer';
@@ -67,6 +68,7 @@ export function LandingPage() {
}
const lenis = new Lenis({ duration: 1.15, smoothWheel: true });
setLenis(lenis);
let raf = 0;
const loop = (time: number) => {
lenis.raf(time);
@@ -79,6 +81,7 @@ export function LandingPage() {
return () => {
cancelAnimationFrame(raf);
lenis.destroy();
setLenis(null);
};
}, [reduced]);
@@ -104,6 +107,7 @@ export function LandingPage() {
<Pricing />
<Estimator />
<Trust />
<Blog />
<FinalCta />
<Footer />
</div>
@@ -1,7 +1,29 @@
import type Lenis from 'lenis';
// Lightweight scroll-progress signal read inside the r3f render loop.
// Kept as a module-level mutable so useFrame can read it without React re-renders.
export const scrollState = { progress: 0 };
// Active Lenis instance (set by LandingPage) so header nav links can drive the
// same smooth-scroll engine. Lenis disables native CSS smooth scroll, so anchor
// jumps must go through it to stay smooth.
export const lenisRef: { current: Lenis | null } = { current: null };
export function setLenis(instance: Lenis | null) {
lenisRef.current = instance;
}
/** Smooth-scroll to a section by id, via Lenis when available (native fallback). */
export function scrollToId(id: string) {
const el = document.getElementById(id);
if (!el) return;
if (lenisRef.current) {
lenisRef.current.scrollTo(el, { offset: -80, duration: 1.1 });
} else {
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
export function setScroll(progress: number) {
scrollState.progress = progress < 0 ? 0 : progress > 1 ? 1 : progress;
}
@@ -0,0 +1,56 @@
'use client';
import { ArrowLeft, BookOpen } from 'lucide-react';
import { useT } from '@/i18n/I18nProvider';
import { Reveal } from '../Reveal';
export function Blog() {
const b = useT().landing.blog;
return (
<section id="blog" className="relative px-6 py-28">
<div className="mx-auto max-w-6xl">
<Reveal className="mb-12 flex justify-center">
<div className="abrban-panel rounded-3xl px-8 py-7 text-center">
<h2 className="abrban-ink flex items-center justify-center gap-2 text-4xl font-bold text-white">
<BookOpen className="h-7 w-7 text-primary-200" />
{b.title}
</h2>
<p className="abrban-ink mt-4 text-lg text-white/85">{b.subtitle}</p>
</div>
</Reveal>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{b.posts.map((post, i) => (
<Reveal key={post.title} delay={(i % 3) * 0.07}>
<article className="abrban-panel group flex h-full flex-col rounded-2xl p-6 transition hover:-translate-y-1 hover:ring-1 hover:ring-primary-400/50">
<div className="flex items-center gap-2">
<span className="inline-flex rounded-full bg-primary-500/25 px-3 py-1 text-xs font-bold text-primary-100 ring-1 ring-primary-400/40">
{post.tag}
</span>
<span className="inline-flex rounded-full bg-amber-400/20 px-2.5 py-1 text-xs font-bold text-amber-100 ring-1 ring-amber-300/40">
{b.comingSoon}
</span>
</div>
<h3 className="abrban-ink mt-5 text-lg font-bold leading-8 text-white">{post.title}</h3>
<p className="mt-2 flex-1 text-sm leading-7 text-white/80">{post.desc}</p>
<span className="mt-5 inline-flex items-center gap-1 text-sm font-semibold text-primary-200/80">
{b.readMore}
<ArrowLeft className="h-4 w-4 ltr:rotate-180" />
</span>
</article>
</Reveal>
))}
</div>
<Reveal className="mt-10 flex justify-center">
<span className="inline-flex items-center gap-2 rounded-xl border border-white/20 bg-white/5 px-7 py-3.5 font-bold text-white/90 backdrop-blur-md">
{b.cta}
<span className="rounded-full bg-amber-400/20 px-2.5 py-0.5 text-xs font-bold text-amber-100 ring-1 ring-amber-300/40">
{b.comingSoon}
</span>
</span>
</Reveal>
</div>
</section>
);
}
@@ -6,7 +6,7 @@ import { useT } from '@/i18n/I18nProvider';
export function HowItWorks() {
const how = useT().landing.how;
return (
<section className="relative px-6 py-28">
<section id="learn" className="relative px-6 py-28">
<div className="mx-auto max-w-5xl">
<Reveal className="mb-16 flex justify-center">
<h2 className="abrban-panel abrban-ink rounded-3xl px-8 py-5 text-4xl font-bold text-white">
@@ -1,13 +1,40 @@
'use client';
import { motion } from 'framer-motion';
import { useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { Menu, X } from 'lucide-react';
import { Link } from '@/i18n/Link';
import { Logo } from '../Logo';
import { useT } from '@/i18n/I18nProvider';
import { useT, useLocale } from '@/i18n/I18nProvider';
import { dirFor } from '@/i18n/config';
import { LanguageSwitcher } from '@/i18n/LanguageSwitcher';
import { scrollToId } from '../scroll-store';
// Section anchors the header navigates to (id ↔ nav dictionary key).
const NAV_ITEMS = [
{ id: 'services', key: 'services' },
{ id: 'pricing', key: 'pricing' },
{ id: 'estimator', key: 'estimator' },
{ id: 'learn', key: 'learn' },
{ id: 'blog', key: 'blog' },
] as const;
export function SiteHeader() {
const t = useT();
const nav = t.landing.nav;
const locale = useLocale();
const isRtl = dirFor(locale) === 'rtl';
const [open, setOpen] = useState(false);
const handleNav = (id: string) => (e: React.MouseEvent) => {
e.preventDefault();
setOpen(false);
scrollToId(id);
};
// Drawer slides in from the inline-start edge (right in RTL, left in LTR).
const offscreen = isRtl ? '100%' : '-100%';
return (
<motion.header
initial={{ opacity: 0, y: -20 }}
@@ -15,24 +42,123 @@ export function SiteHeader() {
transition={{ duration: 0.8, ease: [0.22, 1, 0.36, 1] }}
className="fixed inset-x-0 top-0 z-30"
>
<div className="mx-auto flex max-w-6xl items-center justify-between px-6 py-5">
<div className="mx-auto flex max-w-6xl items-center justify-between gap-3 px-6 py-5">
<Logo className="abrban-ink" />
<div className="flex items-center gap-2 rounded-xl bg-slate-950/30 p-1.5 backdrop-blur-md ring-1 ring-white/10">
<LanguageSwitcher className="text-white/90 hover:text-white" />
<Link
href="/login"
className="rounded-lg px-4 py-2 text-sm font-semibold text-white/90 transition hover:text-white"
{/* Desktop nav — glass pill */}
<nav className="hidden items-center gap-1 rounded-2xl bg-slate-950/30 p-1.5 backdrop-blur-md ring-1 ring-white/10 lg:flex">
{NAV_ITEMS.map((item) => (
<a
key={item.id}
href={`#${item.id}`}
onClick={handleNav(item.id)}
className="rounded-xl px-4 py-2 text-sm font-semibold text-white/85 transition hover:bg-white/10 hover:text-white"
>
{nav[item.key]}
</a>
))}
</nav>
{/* Right controls */}
<div className="flex items-center gap-2">
<div className="hidden items-center gap-2 rounded-xl bg-slate-950/30 p-1.5 backdrop-blur-md ring-1 ring-white/10 sm:flex">
<LanguageSwitcher className="text-white/90 hover:text-white" />
<Link
href="/login"
className="rounded-lg px-4 py-2 text-sm font-semibold text-white/90 transition hover:text-white"
>
{t.common.login}
</Link>
<Link
href="/register"
className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-primary-600/30 transition hover:bg-primary-500"
>
{t.common.register}
</Link>
</div>
{/* Mobile menu toggle */}
<button
type="button"
onClick={() => setOpen(true)}
aria-label={nav.menu}
aria-expanded={open}
className="inline-flex h-11 w-11 items-center justify-center rounded-xl bg-slate-950/30 text-white ring-1 ring-white/10 backdrop-blur-md transition hover:bg-white/10 lg:hidden"
>
{t.common.login}
</Link>
<Link
href="/register"
className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-primary-600/30 transition hover:bg-primary-500"
>
{t.common.register}
</Link>
<Menu className="h-5 w-5" />
</button>
</div>
</div>
{/* Mobile side drawer */}
<AnimatePresence>
{open && (
<>
<motion.div
key="backdrop"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.25 }}
onClick={() => setOpen(false)}
className="fixed inset-0 z-40 bg-slate-950/50 backdrop-blur-sm lg:hidden"
/>
<motion.aside
key="drawer"
initial={{ x: offscreen }}
animate={{ x: 0 }}
exit={{ x: offscreen }}
transition={{ type: 'tween', duration: 0.3, ease: [0.22, 1, 0.36, 1] }}
className={`abrban-panel fixed top-0 z-50 flex h-full w-[82%] max-w-xs flex-col p-5 lg:hidden ${
isRtl ? 'right-0 rounded-l-3xl' : 'left-0 rounded-r-3xl'
}`}
>
<div className="mb-6 flex items-center justify-between">
<Logo className="abrban-ink" />
<button
type="button"
onClick={() => setOpen(false)}
aria-label={t.common.close}
className="inline-flex h-10 w-10 items-center justify-center rounded-xl bg-white/10 text-white ring-1 ring-white/15 transition hover:bg-white/20"
>
<X className="h-5 w-5" />
</button>
</div>
<nav className="flex flex-col gap-1">
{NAV_ITEMS.map((item) => (
<a
key={item.id}
href={`#${item.id}`}
onClick={handleNav(item.id)}
className="rounded-xl px-4 py-3 text-base font-semibold text-white/90 transition hover:bg-white/10"
>
{nav[item.key]}
</a>
))}
</nav>
<div className="my-5 h-px bg-white/10" />
<div className="mt-auto flex flex-col gap-3">
<LanguageSwitcher className="text-white/90 hover:text-white" />
<Link
href="/login"
className="rounded-xl border border-white/20 bg-white/5 px-4 py-3 text-center font-bold text-white backdrop-blur-md transition hover:bg-white/10"
>
{t.common.login}
</Link>
<Link
href="/register"
className="rounded-xl bg-primary-600 px-4 py-3 text-center font-bold text-white shadow-lg shadow-primary-600/30 transition hover:bg-primary-500"
>
{t.common.register}
</Link>
</div>
</motion.aside>
</>
)}
</AnimatePresence>
</motion.header>
);
}
+20
View File
@@ -57,6 +57,14 @@ const en: Dictionary = {
landing: {
badge: 'Self-service cloud platform',
nav: {
services: 'Services',
pricing: 'Pricing',
estimator: 'Estimate',
learn: 'Learn',
blog: 'Blog',
menu: 'Menu',
},
hero: {
title: 'Abrban',
tagline: 'Cloud infrastructure, in your control',
@@ -176,6 +184,18 @@ const en: Dictionary = {
currency: 'Toman',
error: 'Could not calculate cost.',
},
blog: {
title: 'Blog & guides',
subtitle: 'Guides and the latest writing to help you build faster on Abrban.',
comingSoon: 'Coming soon',
cta: 'View all articles',
readMore: 'Read more',
posts: [
{ tag: 'Getting started', title: 'Ship your first app in minutes', desc: 'From connecting a repo to a domain and automatic SSL — step by step.' },
{ tag: 'Cost optimization', title: 'Pick the right resources and pay less', desc: 'Tune CPU, memory, replicas and add-on services wisely.' },
{ tag: 'Security', title: 'Custom domains with automatic SSL', desc: 'Connect your domain and let Abrban issue and renew the certificate.' },
],
},
finalCta: {
titleLead: 'The sky is clear; its time to ',
titleHighlight: 'go live',
+20
View File
@@ -56,6 +56,14 @@ const fa = {
landing: {
badge: 'پلتفرم ابریِ خودسرویس',
nav: {
services: 'سرویس‌ها',
pricing: 'قیمت‌گذاری',
estimator: 'تخمین هزینه',
learn: 'آموزش',
blog: 'بلاگ',
menu: 'منو',
},
hero: {
title: 'ابربان',
tagline: 'زیرساخت ابری، در کنترل تو',
@@ -175,6 +183,18 @@ const fa = {
currency: 'تومان',
error: 'محاسبهٔ هزینه ممکن نشد.',
},
blog: {
title: 'بلاگ و آموزش‌ها',
subtitle: 'راهنماها و تازه‌ترین نوشته‌ها برای ساختن سریع‌تر روی ابربان.',
comingSoon: 'به‌زودی',
cta: 'مشاهدهٔ همهٔ مقالات',
readMore: 'ادامهٔ مطلب',
posts: [
{ tag: 'شروع کار', title: 'اولین اپلیکیشن خود را در چند دقیقه منتشر کن', desc: 'از اتصال ریپازیتوری تا دامنه و SSL خودکار — قدم‌به‌قدم.' },
{ tag: 'بهینه‌سازی هزینه', title: 'منابع را درست انتخاب کن و کمتر هزینه بده', desc: 'CPU، حافظه، رپلیکا و سرویس‌های جانبی را هوشمندانه تنظیم کن.' },
{ tag: 'امنیت', title: 'دامنهٔ اختصاصی و گواهی SSL خودکار', desc: 'دامنه‌ات را وصل کن و بگذار ابربان گواهی را صادر و تمدید کند.' },
],
},
finalCta: {
titleLead: 'آسمان صاف است؛ وقتِ ',
titleHighlight: 'زنده',