feat: add version history drawer with release notes
Introduce a footer-triggered Version History drawer with categorized release notes, hardcoded release data, and semver tracking at v0.3.0. Also ships blog blockquote/table styling, site-wide typography, home page image updates, header refinements, and related bug fixes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+82
-49
@@ -6,6 +6,7 @@ import { Menu, X } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { type } from '@/lib/typography'
|
||||
import { ThemeToggle } from '@/components/theme-toggle'
|
||||
|
||||
const menuItems = [
|
||||
@@ -14,9 +15,16 @@ const menuItems = [
|
||||
{ name: 'راه حلها', href: '/solutions' },
|
||||
{ name: 'مشاوره سازمانی', href: '/consulting' },
|
||||
// { name: 'قیمتگذاری', href: '/pricing' },
|
||||
// { name: 'آزمایشگاه فیپ', href: '/playground' },
|
||||
{ name: 'درباره ما', href: '/about' },
|
||||
]
|
||||
|
||||
/** Outer gutter — matches hero image wrapper (`px-6 md:px-2`). */
|
||||
const siteGutterClass = 'px-6 md:px-2 lg:px-6'
|
||||
|
||||
/** Bar width — matches hero image inner container (`max-w-5xl md:max-w-4xl`). */
|
||||
const headerBarClass = 'mx-auto w-full max-w-5xl md:max-w-4xl lg:max-w-6xl'
|
||||
|
||||
function isLinkActive(pathname: string, href: string): boolean {
|
||||
if (href === '/') return pathname === '/'
|
||||
return pathname === href || pathname.startsWith(href + '/')
|
||||
@@ -34,27 +42,36 @@ export const HeroHeader = () => {
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
return () => window.removeEventListener('scroll', handleScroll)
|
||||
}, [])
|
||||
return (
|
||||
<header>
|
||||
<nav
|
||||
data-state={menuState && 'active'}
|
||||
className="fixed z-20 w-full px-2">
|
||||
<div className={cn('mx-auto mt-2 max-w-6xl px-2 transition-all duration-300 lg:px-2 bg-background/30 backdrop-blur-md rounded-md border border-border/10', isScrolled && 'bg-background/20 max-w-4xl shadow-lg lg:px-5')}>
|
||||
<div className="relative flex flex-wrap items-center justify-between gap-2 lg:gap-0">
|
||||
<div className="flex w-full justify-between lg:w-auto my-2">
|
||||
<Logo />
|
||||
<button
|
||||
onClick={() => setMenuState(!menuState)}
|
||||
aria-label={menuState == true ? 'بستن منو' : 'باز کردن منو'}
|
||||
className="relative z-20 -m-2.5 -mr-4 block cursor-pointer p-2.5 lg:hidden">
|
||||
<Menu className="in-data-[state=active]:rotate-180 in-data-[state=active]:scale-0 in-data-[state=active]:opacity-0 m-auto size-4 duration-200" />
|
||||
<X className="in-data-[state=active]:rotate-0 in-data-[state=active]:scale-100 in-data-[state=active]:opacity-100 absolute inset-0 m-auto size-4 -rotate-180 scale-0 opacity-0 duration-200" />
|
||||
</button>
|
||||
|
||||
React.useEffect(() => {
|
||||
document.body.style.overflow = menuState ? 'hidden' : ''
|
||||
return () => {
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
}, [menuState])
|
||||
|
||||
return (
|
||||
<header className="contents">
|
||||
<nav
|
||||
data-state={menuState ? 'active' : undefined}
|
||||
aria-label="ناوبری اصلی"
|
||||
className={cn(
|
||||
'fixed inset-x-0 top-0 z-20 w-full pt-[max(0.5rem,env(safe-area-inset-top,0px))]',
|
||||
siteGutterClass
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
headerBarClass,
|
||||
'mt-2 rounded-md border border-border/10 bg-background/30 backdrop-blur-md transition-[max-width,box-shadow,background-color] duration-300',
|
||||
isScrolled && 'bg-background/20 shadow-lg lg:max-w-4xl'
|
||||
)}>
|
||||
<div className="relative flex h-14 items-center justify-between gap-3 px-3 sm:gap-4 sm:px-4 lg:h-16">
|
||||
<div className="flex min-w-0 shrink items-center">
|
||||
<Logo />
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-0 m-auto hidden size-fit lg:flex items-center">
|
||||
<ul className="flex gap-2 text-sm">
|
||||
<div className="pointer-events-none absolute inset-x-0 hidden lg:flex lg:items-center lg:justify-center">
|
||||
<ul className={cn("pointer-events-auto flex gap-1", type.nav)}>
|
||||
{menuItems.map((item, index) => {
|
||||
const active = isLinkActive(pathname ?? '', item.href)
|
||||
return (
|
||||
@@ -62,49 +79,65 @@ export const HeroHeader = () => {
|
||||
<Link
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'text-muted-foreground hover:text-foreground hover:bg-accent/20 transition-all duration-300 relative group p-2 px-4 rounded-md',
|
||||
active && 'text-foreground bg-accent/40'
|
||||
'relative rounded-md p-2 px-4 text-muted-foreground transition-all duration-300 hover:bg-accent/20 hover:text-foreground',
|
||||
active && 'bg-accent/40 text-foreground'
|
||||
)}>
|
||||
<span className="relative z-10">{item.name}</span>
|
||||
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="in-data-[state=active]:block lg:in-data-[state=active]:flex mb-3 hidden w-full flex-wrap items-center justify-end space-y-8 md:flex-nowrap lg:m-0 lg:flex lg:w-fit lg:gap-6 lg:space-y-0 lg:border-transparent lg:bg-transparent lg:p-0 lg:shadow-none dark:shadow-none dark:lg:bg-transparent">
|
||||
<div className="lg:hidden w-full">
|
||||
|
||||
<ul className="text-base flex flex-col items-stretch">
|
||||
{menuItems.map((item, index) => {
|
||||
const active = isLinkActive(pathname ?? '', item.href)
|
||||
return (
|
||||
<li key={index}>
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={() => setMenuState(false)}
|
||||
className={cn(
|
||||
'text-muted-foreground block w-full hover:text-foreground hover:bg-accent/20 transition-all duration-300 relative group p-2 px-4 rounded-md',
|
||||
active && 'text-foreground bg-accent/40'
|
||||
)}>
|
||||
<span className="relative z-10">{item.name}</span>
|
||||
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className={`flex justify-end max-md:absolute top-0.5 left-8 in-data-[state=active]:block`}>
|
||||
<div className="flex shrink-0 items-center gap-1 sm:gap-1.5">
|
||||
<ThemeToggle />
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-lg"
|
||||
onClick={() => setMenuState((open) => !open)}
|
||||
aria-expanded={menuState}
|
||||
aria-controls="mobile-nav"
|
||||
aria-label={menuState ? 'بستن منو' : 'باز کردن منو'}
|
||||
className="relative rounded-lg lg:hidden">
|
||||
<Menu
|
||||
aria-hidden
|
||||
className="size-5 transition-all duration-200 in-data-[state=active]:rotate-180 in-data-[state=active]:scale-0 in-data-[state=active]:opacity-0"
|
||||
/>
|
||||
<X
|
||||
aria-hidden
|
||||
className="absolute inset-0 m-auto size-5 -rotate-180 scale-0 opacity-0 transition-all duration-200 in-data-[state=active]:rotate-0 in-data-[state=active]:scale-100 in-data-[state=active]:opacity-100"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="mobile-nav"
|
||||
className={cn(
|
||||
'overflow-hidden border-t border-border/10 transition-[max-height,opacity] duration-300 lg:hidden',
|
||||
menuState ? 'max-h-[min(70vh,28rem)] opacity-100' : 'max-h-0 border-transparent opacity-0'
|
||||
)}>
|
||||
<ul className={cn("flex flex-col px-3 py-2 sm:px-4", type["nav-mobile"])}>
|
||||
{menuItems.map((item, index) => {
|
||||
const active = isLinkActive(pathname ?? '', item.href)
|
||||
return (
|
||||
<li key={index}>
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={() => setMenuState(false)}
|
||||
className={cn(
|
||||
'block w-full rounded-md p-2 px-4 text-muted-foreground transition-all duration-300 hover:bg-accent/20 hover:text-foreground',
|
||||
active && 'bg-accent/40 text-foreground'
|
||||
)}>
|
||||
<span className="relative z-10">{item.name}</span>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
Reference in New Issue
Block a user