feat: redesign landing page with brand system and section blocks
Build & Push Docker Image / build (push) Has been cancelled
Build & Push Docker Image / build (push) Has been cancelled
Refresh the homepage around new hero/services/roadmap sections, shared design tokens, and updated brand assets. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
|
||||
import { Container } from "@/components/container";
|
||||
import { MotionReveal, MotionRevealItem } from "@/components/motion-reveal";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { layout } from "@/lib/layout";
|
||||
import { type } from "@/lib/typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Post {
|
||||
id: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
author: string;
|
||||
published: string;
|
||||
url: string;
|
||||
image: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
interface Blog8Props {
|
||||
heading?: string;
|
||||
description?: string;
|
||||
posts?: Post[];
|
||||
button?: {
|
||||
text: string;
|
||||
url: string;
|
||||
};
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultPosts: Post[] = [
|
||||
{
|
||||
id: "post-1",
|
||||
title: "طراحی رابط کاربری مدرن با کامپوننتهای React",
|
||||
summary:
|
||||
"نگاهی عملی به ساخت رابطهای کاربری مدرن با React و کامپوننتهای آماده؛ از ساختار تا جزئیات تجربه کاربری.",
|
||||
author: "امین رایان",
|
||||
published: "۱۵ بهمن ۱۴۰۳",
|
||||
url: "#",
|
||||
image: "/assets/main-pic/201.jpg",
|
||||
tags: ["طراحی وب", "تجربه کاربری"],
|
||||
},
|
||||
{
|
||||
id: "post-2",
|
||||
title: "سئو و رشد دیده شدن کسبوکار در گوگل",
|
||||
summary:
|
||||
"چطور با بهینهسازی فنی و محتوایی، وبسایتتان در نتایج جستجو بهتر دیده شود و مشتری بیشتری جذب کند.",
|
||||
author: "امین رایان",
|
||||
published: "۲۲ بهمن ۱۴۰۳",
|
||||
url: "#",
|
||||
image: "/assets/main-pic/202.jpg",
|
||||
tags: ["سئو", "بازاریابی دیجیتال"],
|
||||
},
|
||||
{
|
||||
id: "post-3",
|
||||
title: "اتوماسیون فرآیندها برای کسبوکارهای در حال رشد",
|
||||
summary:
|
||||
"چگونه با یکپارچهسازی سیستمها و اتوماسیون کارهای تکراری، تیمتان سریعتر و دقیقتر کار کند.",
|
||||
author: "امین رایان",
|
||||
published: "۵ اسفند ۱۴۰۳",
|
||||
url: "#",
|
||||
image: "/assets/main-pic/203.jpg",
|
||||
tags: ["اتوماسیون", "یکپارچهسازی"],
|
||||
},
|
||||
];
|
||||
|
||||
export function Blog8({
|
||||
heading = "مجله و آخرین اخبار امین رایان",
|
||||
description = "نکات کاربردی درباره طراحی سایت، سئو و رشد آنلاین کسبوکار؛ از تجربه تیم امین رایان.",
|
||||
posts = defaultPosts,
|
||||
button = {
|
||||
text: "مشاهده همه مقالات",
|
||||
url: "#",
|
||||
},
|
||||
className,
|
||||
}: Blog8Props) {
|
||||
return (
|
||||
<section
|
||||
id="blog"
|
||||
className={cn(
|
||||
"section-shell bg-section-canvas",
|
||||
layout.scrollMargin,
|
||||
layout.sectionY,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Container className="section-content flex flex-col gap-12 md:gap-16">
|
||||
<MotionReveal className="w-full">
|
||||
<h2 className={cn(type.h2, "mb-4 md:mb-6")}>{heading}</h2>
|
||||
<p className={cn(type["section-intro"], "max-w-2xl")}>
|
||||
{description}
|
||||
</p>
|
||||
</MotionReveal>
|
||||
|
||||
<MotionReveal
|
||||
stagger
|
||||
className="grid w-full grid-cols-1 gap-8 sm:grid-cols-2 sm:gap-x-6 sm:gap-y-10 md:gap-x-8 md:gap-y-12 lg:gap-x-10"
|
||||
>
|
||||
{posts.map((post) => (
|
||||
<MotionRevealItem key={post.id} className="h-full">
|
||||
<a
|
||||
href={post.url}
|
||||
className={cn(
|
||||
"group flex h-full items-start gap-4 rounded-2xl border border-border/50 p-4 shadow-sm md:gap-5 md:p-5",
|
||||
"bg-background/75 supports-[backdrop-filter]:bg-background/65",
|
||||
"backdrop-blur-xl backdrop-saturate-150",
|
||||
"transition-[border-color,box-shadow,color] duration-200",
|
||||
"hover:border-primary/30 hover:shadow-glow-sm",
|
||||
"dark:hover:border-primary/40",
|
||||
"focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/40"
|
||||
)}
|
||||
>
|
||||
{/* RTL: first = right → تصویر کوچک سمت راست */}
|
||||
<div className="w-1/4 shrink-0">
|
||||
<div className="aspect-square overflow-hidden rounded-xl border border-surface">
|
||||
<img
|
||||
src={post.image}
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<h3
|
||||
className={cn(
|
||||
type.h3,
|
||||
"text-foreground transition-colors duration-200 group-hover:text-primary"
|
||||
)}
|
||||
>
|
||||
{post.title}
|
||||
</h3>
|
||||
|
||||
<p
|
||||
className={cn(
|
||||
type["body-sm"],
|
||||
"mt-2 font-normal text-muted-foreground md:mt-3"
|
||||
)}
|
||||
>
|
||||
{post.summary}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</MotionRevealItem>
|
||||
))}
|
||||
</MotionReveal>
|
||||
|
||||
<MotionReveal className="flex justify-center">
|
||||
<Button asChild size="lg">
|
||||
<a href={button.url}>
|
||||
{button.text}
|
||||
<ArrowLeft />
|
||||
</a>
|
||||
</Button>
|
||||
</MotionReveal>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
+25
-15
@@ -1,7 +1,9 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { Calendar, Clock, ArrowLeft } from "lucide-react";
|
||||
import { Calendar, Clock } from "lucide-react";
|
||||
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
|
||||
import { type } from "@/lib/typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface BlogCardProps {
|
||||
slug: string;
|
||||
@@ -23,9 +25,8 @@ export function BlogCard({
|
||||
image,
|
||||
}: BlogCardProps) {
|
||||
return (
|
||||
<Link href={`/blog/${slug}`} className="group block h-full">
|
||||
<Card className="h-full flex flex-col overflow-hidden transition-all duration-300 hover:shadow-lg hover:-translate-y-1 dark:border-transparent">
|
||||
{/* Image with 3:2 aspect ratio */}
|
||||
<Link href="#" className="group block h-full">
|
||||
<Card className="flex h-full flex-col overflow-hidden transition-all duration-200 hover:-translate-y-1 hover:shadow-lg">
|
||||
<div className="relative aspect-[5/3] w-full overflow-hidden bg-muted">
|
||||
<Image
|
||||
src={image}
|
||||
@@ -35,27 +36,39 @@ export function BlogCard({
|
||||
quality={100}
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 400px"
|
||||
/>
|
||||
{/* Category Badge */}
|
||||
<div className="absolute top-4 right-4">
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-primary/90 text-primary-foreground backdrop-blur-sm">
|
||||
<span
|
||||
className={cn(
|
||||
type.caption,
|
||||
"inline-flex items-center rounded-full bg-primary/90 px-3 py-1 font-medium text-primary-foreground backdrop-blur-sm"
|
||||
)}
|
||||
>
|
||||
{category}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardHeader className="flex-1 flex flex-col">
|
||||
<h3 className="text-base py-2 font-bold leading-tight tracking-normal group-hover:text-primary transition-colors line-clamp-2">
|
||||
<CardHeader className="flex flex-1 flex-col">
|
||||
<h3
|
||||
className={cn(
|
||||
type["card-title"],
|
||||
"py-2 font-bold transition-colors group-hover:text-primary line-clamp-2"
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex-1">
|
||||
<p className="text-muted-foreground leading-relaxed tracking-normal line-clamp-3 text-sm">
|
||||
{excerpt}
|
||||
</p>
|
||||
<p className={cn(type["card-desc"], "line-clamp-3")}>{excerpt}</p>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="flex items-center justify-between gap-4 text-xs text-muted-foreground border-t pt-4">
|
||||
<CardFooter
|
||||
className={cn(
|
||||
type.caption,
|
||||
"flex items-center justify-between gap-4 border-t pt-4"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="size-4" />
|
||||
<span>{date}</span>
|
||||
@@ -69,6 +82,3 @@ export function BlogCard({
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ export function BlogCollapseSection({
|
||||
{item.question}
|
||||
</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className={cn(type.body, "text-justify text-muted-foreground [&_a]:text-accent [&_a]:underline hover:[&_a]:no-underline")}>
|
||||
<AccordionContent className={cn(type.body, "text-muted-foreground [&_a]:text-primary [&_a]:underline hover:[&_a]:no-underline")}>
|
||||
<div dangerouslySetInnerHTML={{ __html: item.answer }} />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
@@ -28,7 +28,7 @@ export function BlogPostCard({
|
||||
|
||||
if (variant === "related") {
|
||||
return (
|
||||
<Link href={`/blog/${post.slug}`} className="block h-full min-w-0">
|
||||
<Link href="#" className="block h-full min-w-0">
|
||||
<Card className="h-full overflow-hidden transition-all rounded-md duration-300 hover:shadow-md flex flex-col p-0 gap-0">
|
||||
<CardContent className="p-3 sm:p-4 grid grid-cols-3 gap-3 sm:gap-4 min-w-0">
|
||||
{thumbnailUrl ? (
|
||||
@@ -76,7 +76,7 @@ export function BlogPostCard({
|
||||
|
||||
if (variant === "tiny") {
|
||||
return (
|
||||
<Link href={`/blog/${post.slug}`} className="block h-full">
|
||||
<Link href="#" className="block h-full">
|
||||
<Card className="h-full overflow-hidden transition-all rounded-md duration-300 hover:shadow-md flex flex-col p-2">
|
||||
<CardContent className="p-2 grid grid-cols-3 gap-4">
|
||||
{thumbnailUrl ? (
|
||||
@@ -123,7 +123,7 @@ export function BlogPostCard({
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={`/blog/${post.slug}`} className="block h-full">
|
||||
<Link href="#" className="block h-full">
|
||||
<Card className="h-full overflow-hidden transition-all duration-300 hover:-translate-y-1 hover:shadow-lg pt-0 gap-1">
|
||||
{thumbnailUrl ? (
|
||||
<div className="relative aspect-video w-full overflow-hidden bg-muted">
|
||||
|
||||
@@ -133,9 +133,9 @@ export function BlogPostMeta({
|
||||
)}
|
||||
|
||||
{/* Divider + actions */}
|
||||
<span className="mr-auto inline-flex items-center gap-1">
|
||||
<span className="ms-auto inline-flex items-center gap-1">
|
||||
<span
|
||||
className="hidden sm:inline h-4 w-px bg-border ml-2"
|
||||
className="hidden sm:inline h-4 w-px bg-border me-2"
|
||||
aria-hidden
|
||||
/>
|
||||
<Tooltip>
|
||||
@@ -147,7 +147,7 @@ export function BlogPostMeta({
|
||||
aria-label="کپی لینک"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="size-4 text-green-600" />
|
||||
<Check className="size-4 text-primary" />
|
||||
) : (
|
||||
<Link2 className="size-4" />
|
||||
)}
|
||||
@@ -166,7 +166,7 @@ export function BlogPostMeta({
|
||||
aria-label="اشتراکگذاری"
|
||||
>
|
||||
{shared ? (
|
||||
<Check className="size-4 text-green-600" />
|
||||
<Check className="size-4 text-primary" />
|
||||
) : (
|
||||
<Share2 className="size-4" />
|
||||
)}
|
||||
|
||||
@@ -18,8 +18,8 @@ const variantConfig = {
|
||||
warn: {
|
||||
icon: AlertTriangle,
|
||||
className:
|
||||
"border-amber-500/50 bg-amber-500/10 text-foreground [&_strong]:text-amber-600 dark:[&_strong]:text-amber-400",
|
||||
iconClassName: "text-amber-500",
|
||||
"border-secondary/40 bg-secondary/10 text-foreground [&_strong]:text-secondary dark:border-primary/40 dark:bg-primary/10 dark:[&_strong]:text-primary",
|
||||
iconClassName: "text-secondary dark:text-primary",
|
||||
},
|
||||
danger: {
|
||||
icon: AlertCircle,
|
||||
@@ -39,7 +39,7 @@ export function BlogTip({ payload }: BlogTipProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"my-6 flex gap-3 rounded-blog border p-4 pr-4 shadow-sm",
|
||||
"my-6 flex gap-3 rounded-blog border p-4 pe-4 shadow-sm",
|
||||
config.className
|
||||
)}
|
||||
role="note"
|
||||
@@ -49,7 +49,7 @@ export function BlogTip({ payload }: BlogTipProps) {
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
className="blog-tip-content min-w-0 text-sm leading-relaxed [&_p]:my-2 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0 [&_a]:text-accent [&_a]:underline hover:[&_a]:no-underline"
|
||||
className="blog-tip-content min-w-0 text-sm leading-relaxed [&_p]:my-2 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0 [&_a]:text-primary [&_a]:underline hover:[&_a]:no-underline"
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+10
-8
@@ -10,6 +10,7 @@ import {
|
||||
} from "@/components/ui/collapsible";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { type } from "@/lib/typography";
|
||||
import type { TocItem } from "@/lib/blog-utils";
|
||||
|
||||
function useScrollProgress() {
|
||||
@@ -68,10 +69,10 @@ const tocLinkClass = (
|
||||
isActive: boolean
|
||||
) =>
|
||||
cn(
|
||||
"block rounded-md border-r-2 rounded-s-none border-transparent py-1.5 text-sm transition-colors hover:text-foreground hover:bg-muted/60",
|
||||
level === 2 && "pr-3",
|
||||
level === 3 && "pr-5",
|
||||
level === 4 && "pr-7",
|
||||
"block rounded-md border-e-2 rounded-s-none border-transparent py-1.5 text-sm leading-snug tracking-normal transition-colors hover:text-foreground hover:bg-muted/60",
|
||||
level === 2 && "pe-3",
|
||||
level === 3 && "pe-5",
|
||||
level === 4 && "pe-7",
|
||||
isActive
|
||||
? "font-medium text-foreground border-primary bg-muted/50"
|
||||
: "text-muted-foreground"
|
||||
@@ -164,7 +165,7 @@ export function BlogToc({ items, className }: BlogTocProps) {
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-0.5 rounded-md border-r-2 rounded-s-none py-1.5 pr-3",
|
||||
"flex items-center gap-0.5 rounded-md border-e-2 rounded-s-none py-1.5 pe-3",
|
||||
activeId === heading.id ? "border-primary bg-muted/50" : "border-transparent"
|
||||
)}
|
||||
>
|
||||
@@ -188,7 +189,7 @@ export function BlogToc({ items, className }: BlogTocProps) {
|
||||
</CollapsibleTrigger>
|
||||
</div>
|
||||
<CollapsibleContent>
|
||||
<div className="space-y-1 pt-0.5 pr-2 border-r border-border/50">
|
||||
<div className="space-y-1 border-e border-border/50 pe-2 pt-0.5">
|
||||
{children.map((item) => renderTocLink(item))}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
@@ -218,7 +219,7 @@ export function BlogToc({ items, className }: BlogTocProps) {
|
||||
style={{ width: `${scrollProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="mb-3 text-md ms-2 font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
<p className={cn(type.eyebrow, "mb-3 ms-2")}>
|
||||
فهرست مطالب
|
||||
</p>
|
||||
<ScrollArea className="h-full pb-8" dir="rtl">
|
||||
@@ -238,7 +239,8 @@ export function BlogToc({ items, className }: BlogTocProps) {
|
||||
|
||||
<CollapsibleTrigger
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between gap-2 rounded-lg border border-border bg-muted/30 px-4 py-3 text-sm font-medium transition-colors hover:bg-muted/50",
|
||||
"flex w-full items-center justify-between gap-2 rounded-lg border border-border bg-muted/30 px-4 py-3 transition-colors hover:bg-muted/50",
|
||||
type.label,
|
||||
mobileOpen && "rounded-b-none border-b-0"
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,36 +1,44 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Container } from '@/components/container'
|
||||
import Link from 'next/link'
|
||||
import { type, typeSpacing } from '@/lib/typography'
|
||||
import { layout } from '@/lib/layout'
|
||||
import { type, typeMeasure } from '@/lib/typography'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export default function CallToAction() {
|
||||
return (
|
||||
<section className="py-16">
|
||||
<div className="mx-auto max-w-5xl rounded-3xl border px-6 py-12 md:py-20 lg:py-32">
|
||||
<div className="text-center">
|
||||
<h2 className={type["cta-title"]}>Start Building</h2>
|
||||
<p className={cn(type.body, "mt-4")}>Libero sapiente aliquam quibusdam aspernatur.</p>
|
||||
<section className={cn("section-shell bg-section-canvas", layout.sectionY)}>
|
||||
<Container>
|
||||
<div className="rounded-3xl border px-6 py-12 md:py-20 lg:py-32">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<h2 className={cn(type["cta-title"], "text-center")}>
|
||||
آماده شروع پروژه هستید؟
|
||||
</h2>
|
||||
<p className={cn(type.body, "mt-4 text-muted-foreground", typeMeasure.prose, "mx-auto text-center")}>
|
||||
با امین رایان مسیر طراحی سایت و سئو را شفاف شروع کنید. مشاوره اولیه رایگان است.
|
||||
</p>
|
||||
|
||||
<div className="mt-12 flex flex-wrap justify-center gap-4">
|
||||
<Button
|
||||
asChild
|
||||
size="lg">
|
||||
<Link href="/solutions">
|
||||
<span>Get Started</span>
|
||||
</Link>
|
||||
</Button>
|
||||
<div className="mt-12 flex flex-wrap justify-center gap-4">
|
||||
<Button
|
||||
asChild
|
||||
size="lg">
|
||||
<Link href="#contact">
|
||||
<span>درخواست مشاوره</span>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
variant="outline">
|
||||
<Link href="/consulting#consulting-form">
|
||||
<span>Book Demo</span>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
variant="secondary">
|
||||
<Link href="#services">
|
||||
<span>مشاهده خدمات</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { type } from "@/lib/typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function formatInlineMarkdown(text: string): string {
|
||||
return text
|
||||
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
||||
@@ -7,7 +10,13 @@ function formatInlineMarkdown(text: string): string {
|
||||
export function ChangelogItem({ text }: { text: string }) {
|
||||
return (
|
||||
<li
|
||||
className="list-disc text-[13px] leading-relaxed text-muted-foreground marker:text-muted-foreground/50 [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[11px] [&_strong]:font-medium [&_strong]:text-foreground/90"
|
||||
className={cn(
|
||||
type["body-sm"],
|
||||
"list-disc text-muted-foreground marker:text-muted-foreground/50",
|
||||
"[&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5",
|
||||
"[&_code]:font-mono [&_code]:text-[0.6875rem]",
|
||||
"[&_strong]:font-medium [&_strong]:text-foreground/90"
|
||||
)}
|
||||
dangerouslySetInnerHTML={{ __html: formatInlineMarkdown(text) }}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
shouldUseParagraphSummary,
|
||||
} from "@/lib/changelog-display";
|
||||
import { ChangelogItem } from "@/components/changelog-item";
|
||||
import { type } from "@/lib/typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { APP_VERSION } from "@/lib/version";
|
||||
|
||||
@@ -24,24 +25,29 @@ export function ChangelogVersionCard({ release }: ChangelogVersionCardProps) {
|
||||
return (
|
||||
<article
|
||||
className={cn(
|
||||
"rounded-xl border bg-card text-left shadow-sm",
|
||||
"rounded-xl border bg-card text-start shadow-sm",
|
||||
isCurrent && "border-primary/30 ring-1 ring-primary/15"
|
||||
)}
|
||||
>
|
||||
<div className="p-4">
|
||||
<header>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h2 className="font-mono text-base font-semibold tracking-tight text-foreground">
|
||||
<h2 className={cn(type.h4, "font-mono tracking-normal text-foreground")}>
|
||||
v{release.version}
|
||||
</h2>
|
||||
{isCurrent && (
|
||||
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-medium text-primary">
|
||||
Current Version
|
||||
<span
|
||||
className={cn(
|
||||
type.micro,
|
||||
"rounded-full bg-primary/10 px-2 py-0.5 font-medium text-primary"
|
||||
)}
|
||||
>
|
||||
نسخه فعلی
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<time
|
||||
className="mt-1.5 block text-xs text-muted-foreground"
|
||||
className={cn(type.caption, "mt-1.5 block")}
|
||||
dateTime={release.date}
|
||||
dir="ltr"
|
||||
>
|
||||
@@ -55,7 +61,7 @@ export function ChangelogVersionCard({ release }: ChangelogVersionCardProps) {
|
||||
aria-hidden
|
||||
/>
|
||||
|
||||
<div className="text-left text-[13px] leading-relaxed text-muted-foreground">
|
||||
<div className={cn(type["body-sm"], "text-muted-foreground")}>
|
||||
{release.summary ? (
|
||||
<p>{release.summary}</p>
|
||||
) : useParagraph ? (
|
||||
@@ -64,7 +70,7 @@ export function ChangelogVersionCard({ release }: ChangelogVersionCardProps) {
|
||||
<div className="space-y-2">
|
||||
{release.sections.map((section) => (
|
||||
<section key={section.title}>
|
||||
<h3 className="mb-1 text-[13px] font-medium text-foreground/80">
|
||||
<h3 className={cn(type.label, "mb-1 text-foreground/80")}>
|
||||
{CHANGELOG_SECTION_LABELS[section.title]}
|
||||
</h3>
|
||||
<ul className="space-y-1 ps-4">
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
'use client'
|
||||
|
||||
import { usePathname } from 'next/navigation'
|
||||
import FooterSection from '@/components/footer'
|
||||
import { HeroHeader } from '@/components/header'
|
||||
import type { ChangelogRelease } from '@/lib/changelog-types'
|
||||
import { PageAtmosphere, PageGrid } from '@/components/page-atmosphere'
|
||||
|
||||
export function ConditionalLayout({
|
||||
children,
|
||||
changelogReleases,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
changelogReleases: ChangelogRelease[]
|
||||
}) {
|
||||
const pathname = usePathname()
|
||||
const isLoginPage = pathname === '/login'
|
||||
const showChrome = !isLoginPage
|
||||
|
||||
return (
|
||||
<>
|
||||
{showChrome && <HeroHeader />}
|
||||
{children}
|
||||
{showChrome && <FooterSection releases={changelogReleases} />}
|
||||
<PageAtmosphere />
|
||||
<div className="page-content relative">
|
||||
<PageGrid />
|
||||
<div className="relative z-[1]">
|
||||
<HeroHeader />
|
||||
{children}
|
||||
<FooterSection />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@ import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { CheckCircle2, Loader2 } from 'lucide-react'
|
||||
|
||||
const consultingAreas = [
|
||||
'اینتگریشن',
|
||||
'یکپارچهسازی سامانهها',
|
||||
'هوشمندسازی',
|
||||
'RAG',
|
||||
'Performance',
|
||||
'جستجوی معنایی و دانشبنیان',
|
||||
'بهینهسازی عملکرد',
|
||||
'امنیت',
|
||||
'معماری نرمافزار',
|
||||
'داده و تحلیل',
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { layout } from "@/lib/layout";
|
||||
|
||||
type ContainerProps = ComponentProps<"div">;
|
||||
|
||||
/**
|
||||
* Global content container. Use inside full-bleed sections so backgrounds
|
||||
* can span the viewport while content stays aligned with the rest of the site.
|
||||
*/
|
||||
export function Container({ className, ...props }: ContainerProps) {
|
||||
return <div className={cn(layout.container, className)} {...props} />;
|
||||
}
|
||||
@@ -1,20 +1,22 @@
|
||||
import Image from "next/image";
|
||||
import { Container } from "@/components/container";
|
||||
import { getHomeImages } from "@/lib/home-images";
|
||||
import { type, typeSpacing } from "@/lib/typography";
|
||||
import { layout } from "@/lib/layout";
|
||||
import { type } from "@/lib/typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function ContentSection() {
|
||||
const { ecosystem } = getHomeImages();
|
||||
|
||||
return (
|
||||
<section className="bg-background py-16 md:py-32">
|
||||
<div className="mx-auto max-w-5xl space-y-8 px-6 md:space-y-16">
|
||||
<section className={cn("section-shell bg-section-canvas", layout.sectionY)}>
|
||||
<Container className="space-y-8 md:space-y-16">
|
||||
<h2 className={cn("relative z-10 max-w-xl", type.h2)}>
|
||||
اکوسیستم فیپ: راهکارهای یکپارچه برای کسبوکارهای هوشمند
|
||||
</h2>
|
||||
<div className="grid gap-6 sm:grid-cols-2 md:gap-12 lg:gap-24">
|
||||
<div className="relative mb-6 sm:mb-0">
|
||||
<div className="bg-linear-to-b aspect-[3/2] relative overflow-hidden rounded-2xl from-zinc-300 to-transparent p-px dark:from-zinc-700">
|
||||
<div className="bg-linear-to-b aspect-[3/2] relative overflow-hidden rounded-2xl from-brand-light to-transparent p-px dark:from-brand-gray/40">
|
||||
<div className="relative size-full overflow-hidden rounded-[15px] bg-muted">
|
||||
<Image
|
||||
src={ecosystem.light.src}
|
||||
@@ -39,14 +41,14 @@ export default function ContentSection() {
|
||||
</div>
|
||||
|
||||
<div className="relative space-y-4">
|
||||
<p className={cn(type.body, "text-muted-foreground text-justify sm:text-start")}>
|
||||
<p className={cn(type.body, "text-muted-foreground")}>
|
||||
فیپ یک گروه متخصص است که در حوزه اینتگریشن و روشهای نوین یکپارچهسازی فعالیت میکند.{" "}
|
||||
<span className="text-primary font-bold">
|
||||
ما با استفاده از تکنولوژیهای پیشرفته و رویکردهای نوآورانه
|
||||
</span>{" "}
|
||||
به کسبوکارها کمک میکنیم تا سیستمهای خود را به شکلی کارآمد و قابلاعتماد بهم متصل کنند.
|
||||
</p>
|
||||
<p className={cn(type.body, "text-muted-foreground text-justify sm:text-start")}>
|
||||
<p className={cn(type.body, "text-muted-foreground")}>
|
||||
ما در زمینه یکپارچهسازی سیستمها، اتوماسیون فرآیندها، ارتباطات چندکاناله و مدیریت دادهها تخصص داریم. با استفاده از روشهای نوین و ابزارهای پیشرفته، راهکارهای سفارشی برای هر کسبوکار طراحی و پیادهسازی میکنیم که به آنها امکان میدهد تا از قدرت اتوماسیون و ارتباطات هوشمند بهرهمند شوند.
|
||||
</p>
|
||||
|
||||
@@ -64,7 +66,7 @@ export default function ContentSection() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Container } from "@/components/container";
|
||||
import { MotionReveal } from "@/components/motion-reveal";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { layout } from "@/lib/layout";
|
||||
import { type } from "@/lib/typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Cta10Props {
|
||||
heading?: string;
|
||||
description?: string;
|
||||
buttons?: {
|
||||
primary?: {
|
||||
text: string;
|
||||
url: string;
|
||||
};
|
||||
secondary?: {
|
||||
text: string;
|
||||
url: string;
|
||||
};
|
||||
};
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Cta10({
|
||||
heading = "آماده شروع پروژه هستید؟",
|
||||
description = "با امین رایان طراحی سایت و سئو را شروع کنید. مشاوره اولیه رایگان است و مسیر رشد آنلاین کسبوکارتان را با هم مشخص میکنیم.",
|
||||
buttons = {
|
||||
primary: {
|
||||
text: "درخواست مشاوره",
|
||||
url: "#",
|
||||
},
|
||||
secondary: {
|
||||
text: "مشاهده خدمات",
|
||||
url: "#services",
|
||||
},
|
||||
},
|
||||
className,
|
||||
}: Cta10Props) {
|
||||
return (
|
||||
<section
|
||||
id="contact"
|
||||
className={cn(
|
||||
"section-shell bg-section-canvas",
|
||||
layout.scrollMargin,
|
||||
layout.sectionY,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Container className="section-content relative">
|
||||
<div className="local-highlight" aria-hidden />
|
||||
<MotionReveal className="bg-gradient-cta relative z-[1] isolate flex w-full flex-col gap-8 overflow-hidden rounded-xl p-6 text-white shadow-floating sm:gap-10 sm:p-8 md:rounded-2xl lg:flex-row lg:items-center lg:gap-16 lg:p-12">
|
||||
<span className="cta-diamond-pattern cta-diamond-pattern--left" aria-hidden />
|
||||
<div className="relative z-[1] min-w-0 flex-1">
|
||||
<h2 className={cn(type.h2, "mb-3 text-white md:mb-4 lg:mb-6")}>
|
||||
{heading}
|
||||
</h2>
|
||||
<p className={cn(type["section-intro"], "max-w-xl text-brand-light/90")}>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative z-[1] flex w-full shrink-0 flex-col gap-2 sm:w-auto sm:flex-row">
|
||||
{buttons.primary && (
|
||||
<Button asChild size="lg" surface="brand" className="w-full sm:w-auto">
|
||||
<a href={buttons.primary.url}>{buttons.primary.text}</a>
|
||||
</Button>
|
||||
)}
|
||||
{buttons.secondary && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
surface="brand"
|
||||
size="lg"
|
||||
asChild
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
<a href={buttons.secondary.url}>{buttons.secondary.text}</a>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</MotionReveal>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Container } from "@/components/container";
|
||||
import { MotionReveal } from "@/components/motion-reveal";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { layout } from "@/lib/layout";
|
||||
import { type } from "@/lib/typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Cta16Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Cta16({ className }: Cta16Props) {
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
"section-shell bg-section-canvas",
|
||||
layout.scrollMargin,
|
||||
layout.sectionY,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Container className="section-content relative">
|
||||
<div className="local-highlight" aria-hidden />
|
||||
<div
|
||||
className={cn(
|
||||
"bg-cta-photo-banner relative z-[1] isolate flex min-h-[11.2rem] items-center justify-center overflow-hidden rounded-xl shadow-floating md:min-h-[15.5rem] md:rounded-2xl"
|
||||
)}
|
||||
>
|
||||
<span className="cta-diamond-pattern" aria-hidden />
|
||||
<div className="relative z-[1] w-full p-4 sm:p-3 md:p-4">
|
||||
<MotionReveal className="flex flex-col gap-4 text-center text-white">
|
||||
<div className="flex flex-col gap-3 sm:gap-4">
|
||||
<h2 className={cn(type["cta-title"], "text-center text-white")}>
|
||||
مشاهده کاتالوگ امین رایان
|
||||
</h2>
|
||||
<p
|
||||
className={cn(
|
||||
type["section-intro"],
|
||||
"mx-auto max-w-2xl text-center text-brand-light/90"
|
||||
)}
|
||||
>
|
||||
در یک نگاه با شرکت امین رایان آشنا شوید
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center gap-2 sm:flex-row">
|
||||
<Button size="lg" surface="brand" className="w-full sm:w-auto" asChild>
|
||||
<a href="#contact">دریافت کاتالوگ</a>
|
||||
</Button>
|
||||
</div>
|
||||
</MotionReveal>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
+32
-22
@@ -1,48 +1,57 @@
|
||||
'use client'
|
||||
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
|
||||
import { Container } from '@/components/container'
|
||||
import Link from 'next/link'
|
||||
import { type, typeSpacing } from '@/lib/typography'
|
||||
import { layout } from '@/lib/layout'
|
||||
import { type, typeMeasure } from '@/lib/typography'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export default function FAQsFour() {
|
||||
const faqItems = [
|
||||
{
|
||||
id: 'item-1',
|
||||
question: 'How long does shipping take?',
|
||||
answer: 'Standard shipping takes 3-5 business days, depending on your location. Express shipping options are available at checkout for 1-2 business day delivery.',
|
||||
question: 'طراحی سایت چقدر زمان میبرد؟',
|
||||
answer:
|
||||
'بسته به پیچیدگی پروژه، معمولاً بین ۲ تا ۶ هفته. پس از جلسه مشاوره اولیه، زمانبندی دقیق و مراحل کار را شفاف اعلام میکنیم.',
|
||||
},
|
||||
{
|
||||
id: 'item-2',
|
||||
question: 'What payment methods do you accept?',
|
||||
answer: 'We accept all major credit cards (Visa, Mastercard, American Express), PayPal, Apple Pay, and Google Pay. For enterprise customers, we also offer invoicing options.',
|
||||
question: 'هزینه طراحی سایت و سئو چگونه محاسبه میشود؟',
|
||||
answer:
|
||||
'هزینه بر اساس دامنه پروژه، تعداد صفحات، امکانات اختصاصی و سطح سئو تعیین میشود. مشاوره اولیه رایگان است و پیشنهاد قیمت شفاف ارائه میدهیم.',
|
||||
},
|
||||
{
|
||||
id: 'item-3',
|
||||
question: 'Can I change or cancel my order?',
|
||||
answer: 'You can modify or cancel your order within 1 hour of placing it. After this window, please contact our customer support team who will assist you with any changes.',
|
||||
question: 'آیا بعد از تحویل سایت پشتیبانی دارید؟',
|
||||
answer:
|
||||
'بله. بستههای پشتیبانی ماهانه شامل بهروزرسانی، رفع باگ، پشتیبانگیری و بهبودهای جزئی است تا سایت همیشه پایدار بماند.',
|
||||
},
|
||||
{
|
||||
id: 'item-4',
|
||||
question: 'Do you ship internationally?',
|
||||
answer: "Yes, we ship to over 50 countries worldwide. International shipping typically takes 7-14 business days. Additional customs fees may apply depending on your country's import regulations.",
|
||||
question: 'سئو از چه زمانی نتیجه میدهد؟',
|
||||
answer:
|
||||
'معمولاً پس از ۲ تا ۴ ماه اولین نشانههای بهبود دیده میشود. نتیجه پایدار به کیفیت محتوا، رقابت کلمات کلیدی و استمرار بهینهسازی بستگی دارد.',
|
||||
},
|
||||
{
|
||||
id: 'item-5',
|
||||
question: 'What is your return policy?',
|
||||
answer: 'We offer a 30-day return policy for most items. Products must be in original condition with tags attached. Some specialty items may have different return terms, which will be noted on the product page.',
|
||||
question: 'آیا سایت برای موبایل و سرعت بهینه میشود؟',
|
||||
answer:
|
||||
'بله. تمام پروژهها واکنشگرا طراحی میشوند و روی سرعت بارگذاری، تجربه کاربری و استانداردهای فنی گوگل تمرکز داریم.',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="py-16 md:py-24">
|
||||
<div className="mx-auto max-w-5xl px-4 md:px-6">
|
||||
<div className="mx-auto max-w-xl text-center">
|
||||
<h2 className={type.h2}>Frequently Asked Questions</h2>
|
||||
<p className={cn(type.body, "text-muted-foreground mt-4 text-balance")}>Discover quick and comprehensive answers to common questions about our platform, services, and features.</p>
|
||||
<section className={cn("section-shell bg-section-canvas", layout.sectionY)}>
|
||||
<Container>
|
||||
<div className={cn("mx-auto text-start", typeMeasure.narrow)}>
|
||||
<h2 className={type.h2}>سؤالات متداول</h2>
|
||||
<p className={cn(type["section-intro"], "mt-4")}>
|
||||
پاسخهای کوتاه به پرسشهای رایج درباره طراحی سایت، سئو و پشتیبانی در امین رایان.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mt-12 max-w-xl">
|
||||
<div className={cn("mx-auto mt-12", typeMeasure.narrow)}>
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
@@ -65,15 +74,16 @@ export default function FAQsFour() {
|
||||
</Accordion>
|
||||
|
||||
<p className={cn(type.body, "text-muted-foreground mt-6 px-8")}>
|
||||
Can't find what you're looking for? Contact our{' '}
|
||||
پاسخ سؤالتان را پیدا نکردید؟ با{' '}
|
||||
<Link
|
||||
href="/consulting#consulting-form"
|
||||
className="text-accent font-medium hover:underline">
|
||||
customer support team
|
||||
href="#contact"
|
||||
className="font-medium text-primary hover:underline">
|
||||
تیم مشاوره
|
||||
</Link>
|
||||
{' '}تماس بگیرید.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Container } from "@/components/container";
|
||||
import { MotionReveal, MotionRevealItem } from "@/components/motion-reveal";
|
||||
import { BorderBeam } from "@/components/ui/border-beam";
|
||||
import { layout } from "@/lib/layout";
|
||||
import { type } from "@/lib/typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface FeatureItem {
|
||||
id: number;
|
||||
title: string;
|
||||
image: string;
|
||||
description: string[];
|
||||
}
|
||||
|
||||
interface Feature197Props {
|
||||
features?: FeatureItem[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultFeatures: FeatureItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: "به صرفه",
|
||||
image: "/assets/main-pic/55.jpg",
|
||||
description: [
|
||||
"داشتن وبسایت حرفهای نباید هزینههای سنگین داشته باشد. ما بستههای متنوع و اقتصادی متناسب با بودجه هر کسبوکار ارائه میدهیم تا با سرمایهگذاری منطقی، حضوری قوی و قابلاعتماد در فضای آنلاین داشته باشید.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "دانش بالا",
|
||||
image: "/assets/main-pic/66.jpg",
|
||||
description: [
|
||||
"تیم ما متشکل از طراحان، برنامهنویسان و متخصصان سئو است که با استانداردهای روز وب کار میکنند. هدفمان ساخت سایتی سریع، امن و کاربرپسند است که به رشد برند و جذب مشتری کمک کند.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "رضایت مشتری",
|
||||
image: "/assets/main-pic/77.jpg",
|
||||
description: [
|
||||
"از ابتدای پروژه تا پس از تحویل، شفاف و منظم کنار شما هستیم. نیازها و انتظاراتتان را دقیق بررسی میکنیم و با پشتیبانی متعهدانه، تجربهای مطمئن و نتیجهمحور برای رشد کسبوکارتان میسازیم.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "سریع و باکیفیت",
|
||||
image: "/assets/main-pic/88.jpg",
|
||||
description: [
|
||||
"پروژه را در کوتاهترین زمان ممکن و بدون افت کیفیت تحویل میدهیم. از ظاهر کاربرپسند تا کدنویسی استاندارد و سئو، دقت در جزئیات برای ما اصل غیرقابل مذاکره است.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "تجربه بالا",
|
||||
image: "/assets/main-pic/99.jpg",
|
||||
description: [
|
||||
"سالها همکاری با کسبوکارهای متنوع به ما کمک کرده نیاز هر صنعت را درست بشناسیم و از اشتباهات رایج جلوگیری کنیم. این سابقه، از مشاوره تا تحویل نهایی، کیفیت و اطمینان پروژه را تضمین میکند.",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function Feature197({
|
||||
features = defaultFeatures,
|
||||
className,
|
||||
}: Feature197Props) {
|
||||
const [activeTabId, setActiveTabId] = useState<number | null>(features[0]?.id ?? 1);
|
||||
const [activeImage, setActiveImage] = useState(features[0]?.image ?? "");
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
"section-shell bg-section-lift",
|
||||
layout.scrollMargin,
|
||||
layout.sectionY,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Container className="section-content">
|
||||
<MotionReveal stagger>
|
||||
<MotionRevealItem>
|
||||
<h2 className={cn(type.h2, "mb-12")}>چرا امین رایان؟</h2>
|
||||
</MotionRevealItem>
|
||||
<MotionRevealItem>
|
||||
<div className="flex w-full flex-col items-start justify-between gap-10 md:gap-12 lg:flex-row">
|
||||
<div className="w-full min-w-0 lg:w-1/2">
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
className={cn(
|
||||
"w-full",
|
||||
// Soften default 200ms accordion height motion for this section
|
||||
"[&_[data-slot=accordion-content][data-state=open]]:[animation-duration:560ms]",
|
||||
"[&_[data-slot=accordion-content][data-state=closed]]:[animation-duration:440ms]",
|
||||
"[&_[data-slot=accordion-content]]:[animation-timing-function:cubic-bezier(0.22,1,0.36,1)]"
|
||||
)}
|
||||
defaultValue={`item-${features[0]?.id ?? 1}`}
|
||||
>
|
||||
{features.map((tab) => (
|
||||
<AccordionItem
|
||||
key={tab.id}
|
||||
value={`item-${tab.id}`}
|
||||
className="transition-opacity duration-300 hover:opacity-80"
|
||||
>
|
||||
<AccordionTrigger
|
||||
onClick={() => {
|
||||
setActiveImage(tab.image);
|
||||
setActiveTabId(tab.id);
|
||||
}}
|
||||
className="cursor-pointer py-4 no-underline! transition-colors duration-300 hover:no-underline sm:py-5 [&[data-state=open]>svg]:duration-500 [&>svg]:transition-transform [&>svg]:duration-500"
|
||||
>
|
||||
<h3
|
||||
className={cn(
|
||||
type.h3,
|
||||
"transition-colors duration-500 ease-out",
|
||||
tab.id === activeTabId
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{tab.title}
|
||||
</h3>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-2">
|
||||
<div
|
||||
className={cn(
|
||||
type.body,
|
||||
"flex flex-col gap-3 text-muted-foreground",
|
||||
"animate-in fade-in-0 slide-in-from-top-1 duration-500 ease-out fill-mode-both"
|
||||
)}
|
||||
>
|
||||
{tab.description.map((paragraph, index) => (
|
||||
<p key={`${tab.id}-${index}`}>{paragraph}</p>
|
||||
))}
|
||||
</div>
|
||||
<div className="media-frame relative mt-4 overflow-hidden rounded-xl lg:hidden">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={tab.image}
|
||||
alt={tab.title}
|
||||
className="h-full max-h-64 w-full rounded-[inherit] object-cover animate-in fade-in-0 zoom-in-[0.985] duration-700 ease-out fill-mode-both sm:max-h-80"
|
||||
/>
|
||||
<BorderBeam
|
||||
duration={6}
|
||||
size={160}
|
||||
borderWidth={1}
|
||||
colorFrom="#29A19C"
|
||||
colorTo="#044343"
|
||||
/>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
<div className="relative hidden w-1/2 min-w-0 lg:block">
|
||||
<div className="local-highlight local-highlight--soft" aria-hidden />
|
||||
<div className="media-frame relative z-[1] rounded-xl bg-muted/40">
|
||||
<div className="relative aspect-4/3 overflow-hidden rounded-[inherit]">
|
||||
{features.map((feature) => (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
key={feature.id}
|
||||
src={feature.image}
|
||||
alt={feature.title}
|
||||
className={cn(
|
||||
"absolute inset-0 size-full object-cover transition-opacity duration-700 ease-out",
|
||||
activeImage === feature.image
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<BorderBeam
|
||||
duration={6}
|
||||
size={200}
|
||||
borderWidth={1}
|
||||
colorFrom="#29A19C"
|
||||
colorTo="#044343"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MotionRevealItem>
|
||||
</MotionReveal>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -4,8 +4,10 @@ import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { ChevronLeft, ImageIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Container } from "@/components/container";
|
||||
import { getFeaturedImageUrl, type WpPost } from "@/lib/wp-api";
|
||||
import { formatDateShamsi } from "@/lib/format-date";
|
||||
import { layout } from "@/lib/layout";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { type } from "@/lib/typography";
|
||||
|
||||
@@ -27,7 +29,7 @@ function PostCard({
|
||||
const date = post.date ? formatDateShamsi(post.date) : null;
|
||||
const authorName = post._embedded?.author?.[0]?.name ?? null;
|
||||
const thumbnailUrl = getFeaturedImageUrl(post, size === "large" ? "medium_large" : "medium");
|
||||
const href = `/blog/${post.slug}`;
|
||||
const href = "#";
|
||||
|
||||
return (
|
||||
<Link
|
||||
@@ -81,7 +83,7 @@ function PostCardRow({ post }: { post: WpPost }) {
|
||||
const date = post.date ? formatDateShamsi(post.date) : null;
|
||||
const authorName = post._embedded?.author?.[0]?.name ?? null;
|
||||
const thumbnailUrl = getFeaturedImageUrl(post, "medium");
|
||||
const href = `/blog/${post.slug}`;
|
||||
const href = "#";
|
||||
|
||||
return (
|
||||
<Link
|
||||
@@ -103,7 +105,7 @@ const DEFAULT_TITLE = "از دانشنامه";
|
||||
const DEFAULT_CAPTION =
|
||||
"از داده تا بینش؛ یادداشتها و تجربههایی که مسیر شما را روشنتر میکنند.";
|
||||
const DEFAULT_CTA_LABEL = "مشاهده همه مقالات";
|
||||
const DEFAULT_CTA_HREF = "/blog";
|
||||
const DEFAULT_CTA_HREF = "#";
|
||||
|
||||
interface FeaturedPostsGridProps {
|
||||
posts: WpPost[];
|
||||
@@ -113,7 +115,7 @@ interface FeaturedPostsGridProps {
|
||||
caption?: string;
|
||||
/** متن دکمه CTA */
|
||||
ctaLabel?: string;
|
||||
/** لینک CTA (پیشفرض: /blog) */
|
||||
/** لینک CTA (پیشفرض: # — آزمایشی) */
|
||||
ctaHref?: string;
|
||||
}
|
||||
|
||||
@@ -129,13 +131,16 @@ export function FeaturedPostsGrid({
|
||||
const [first, second, third, fourth] = posts;
|
||||
|
||||
return (
|
||||
<section className="bg-background py-16 md:py-24" aria-label={title}>
|
||||
<div className="mx-auto max-w-6xl px-4 sm:px-6">
|
||||
<header className="mb-10 text-center md:mb-12">
|
||||
<h2 className={type.h2}>
|
||||
<section
|
||||
className={cn("section-shell bg-section-canvas", layout.sectionY)}
|
||||
aria-label={title}
|
||||
>
|
||||
<Container>
|
||||
<header className="mb-10 md:mb-12">
|
||||
<h2 className={cn(type.h2, "text-center")}>
|
||||
{title}
|
||||
</h2>
|
||||
<p className={cn(type["body-sm"], "mx-auto mt-3 max-w-2xl text-muted-foreground md:text-base")}>
|
||||
<p className={cn(type["section-intro"], "mx-auto mt-3 max-w-2xl")}>
|
||||
{caption}
|
||||
</p>
|
||||
</header>
|
||||
@@ -164,14 +169,14 @@ export function FeaturedPostsGrid({
|
||||
<p className={type.caption}>
|
||||
مطالب بیشتر در دانشنامه
|
||||
</p>
|
||||
<Button asChild size="lg" variant="outline" className="rounded-full px-6 font-medium shadow-sm transition-all hover:shadow-md">
|
||||
<Link href={ctaHref} className="inline-flex items-center gap-2">
|
||||
<Button asChild size="lg">
|
||||
<Link href={ctaHref}>
|
||||
{ctaLabel}
|
||||
<ChevronLeft className="size-4" aria-hidden />
|
||||
<ChevronLeft aria-hidden />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import { BorderBeam } from "./ui/border-beam";
|
||||
import { Container } from "@/components/container";
|
||||
import type { FeatureImageKey, FeatureImageSet } from "@/lib/home-images";
|
||||
import { layout } from "@/lib/layout";
|
||||
import { type, typeSpacing } from "@/lib/typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -28,11 +30,11 @@ export function FeaturesClient({ images }: FeaturesClientProps) {
|
||||
const active = images[activeItem];
|
||||
|
||||
return (
|
||||
<section className="bg-background py-12 md:py-20 lg:py-32">
|
||||
<div className="bg-linear-to-b absolute inset-0 -z-10 sm:inset-6 sm:rounded-b-3xl dark:block dark:to-[color-mix(in_oklab,var(--color-zinc-900)_75%,var(--color-background))]"></div>
|
||||
<div className="mx-auto max-w-5xl space-y-8 px-6 md:space-y-16 lg:space-y-20 dark:[--color-border:color-mix(in_oklab,var(--color-white)_10%,transparent)]">
|
||||
<div className={cn("relative z-10 mx-auto max-w-2xl text-center", typeSpacing.sectionStack)}>
|
||||
<h2 className={type.h2}>
|
||||
<section className={cn("section-shell relative bg-section-canvas", layout.sectionY)}>
|
||||
<div className="bg-linear-to-b absolute inset-0 -z-10 from-muted/40 to-background sm:inset-6 sm:rounded-b-3xl dark:from-surface dark:to-background"></div>
|
||||
<Container className="space-y-8 md:space-y-16 lg:space-y-20 dark:[--color-border:color-mix(in_oklab,var(--color-white)_10%,transparent)]">
|
||||
<div className={cn("relative z-10 mx-auto max-w-2xl", typeSpacing.sectionStack)}>
|
||||
<h2 className={cn(type.h2, "text-center")}>
|
||||
پایه و اساس ارتباطات هوشمند
|
||||
</h2>
|
||||
<p className={type.body}>
|
||||
@@ -56,7 +58,7 @@ export function FeaturesClient({ images }: FeaturesClientProps) {
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<p className={type.body}>
|
||||
اتصال و هماهنگسازی سیستمها و سرویسهای مختلف برای ایجاد جریان یکپارچه داده و فرآیند. ما به شما امکان میدهیم تا به سرویسها و APIهای مختلف متصل شوید، دادهها را به صورت خودکار بین سیستمها همگامسازی کنید و workflowهای یکپارچه ایجاد کنید.
|
||||
اتصال و هماهنگسازی سیستمها و سرویسهای مختلف برای ایجاد جریان یکپارچه داده و فرآیند. ما به شما امکان میدهیم تا به سرویسها و رابطهای برنامهنویسی مختلف متصل شوید، دادهها را به صورت خودکار بین سیستمها همگامسازی کنید و گردشکارهای یکپارچه ایجاد کنید.
|
||||
</p>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
@@ -69,7 +71,7 @@ export function FeaturesClient({ images }: FeaturesClientProps) {
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<p className={type.body}>
|
||||
ارائه پنلهای کاربری پیشرفته برای نمایش آمار، تحلیل دادهها و مدیریت اطلاعات با رابط کاربری حرفهای. داشبوردهای تعاملی و قابل تنظیم، نمایش آمار و گزارشهای زنده، و امکان ویرایش و مدیریت اطلاعات از طریق UI قدرتمند.
|
||||
ارائه پنلهای کاربری پیشرفته برای نمایش آمار، تحلیل دادهها و مدیریت اطلاعات با رابط کاربری حرفهای. داشبوردهای تعاملی و قابل تنظیم، نمایش آمار و گزارشهای زنده، و امکان ویرایش و مدیریت اطلاعات از طریق رابط کاربری قدرتمند.
|
||||
</p>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
@@ -95,15 +97,15 @@ export function FeaturesClient({ images }: FeaturesClientProps) {
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<p className={type.body}>
|
||||
ایجاد کارهای خودکار برای اتوماتیکسازی فرآیندهای کسبوکار و کاهش کارهای دستی. تعریف و اجرای workflowهای خودکار، اتوماسیون بر اساس رویداد و زمان، زمانبندی و اجرای کارهای تکراری، و اعلانها و هشدارهای خودکار.
|
||||
ایجاد کارهای خودکار برای اتوماتیکسازی فرآیندهای کسبوکار و کاهش کارهای دستی. تعریف و اجرای گردشکارهای خودکار، اتوماسیون بر اساس رویداد و زمان، زمانبندی و اجرای کارهای تکراری، و اعلانها و هشدارهای خودکار.
|
||||
</p>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
<div className="relative rounded-3xl bg-linear-to-br from-primary/50 via-accent/40 to-secondary/50 p-px shadow-lg shadow-primary/10">
|
||||
<div className="bg-background relative flex overflow-hidden rounded-[calc(1.5rem-1px)] m-0 p-0">
|
||||
<div className="w-15 absolute inset-0 right-0 ml-auto border-l border-primary/15 bg-[repeating-linear-gradient(-45deg,color-mix(in_oklab,var(--color-primary)_25%,var(--color-border)),color-mix(in_oklab,var(--color-primary)_25%,var(--color-border))_1px,transparent_1px,transparent_8px)]"></div>
|
||||
<div className="bg-background relative flex rounded-[calc(1.5rem-1px)] m-0 p-0">
|
||||
<div className="w-15 absolute inset-0 end-0 ms-auto overflow-hidden rounded-[inherit] border-s border-primary/15 bg-[repeating-linear-gradient(-45deg,color-mix(in_oklab,var(--color-primary)_25%,var(--color-border)),color-mix(in_oklab,var(--color-primary)_25%,var(--color-border))_1px,transparent_1px,transparent_8px)]"></div>
|
||||
<div
|
||||
className="bg-background relative w-full rounded-[calc(1.5rem-1px)] m-0 p-0"
|
||||
style={{ aspectRatio: `${active.image.width} / ${active.image.height}` }}
|
||||
@@ -142,13 +144,13 @@ export function FeaturesClient({ images }: FeaturesClientProps) {
|
||||
duration={6}
|
||||
size={200}
|
||||
borderWidth={1}
|
||||
colorFrom="#FCC61D"
|
||||
colorTo="#01796F"
|
||||
colorFrom="#29A19C"
|
||||
colorTo="#044343"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
+179
-36
@@ -1,51 +1,194 @@
|
||||
import { Logo } from "@/components/logo";
|
||||
import {
|
||||
Facebook,
|
||||
Instagram,
|
||||
Linkedin,
|
||||
Twitter,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ChangelogPanel } from "@/components/changelog-panel";
|
||||
import type { ChangelogRelease } from "@/lib/changelog-types";
|
||||
|
||||
import { Container } from "@/components/container";
|
||||
import { Logo } from "@/components/logo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { type } from "@/lib/typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const links = [
|
||||
const aboutText =
|
||||
"شرکت امین رایان اسپادان از سال ۹۱ فعالیت خود را شروع کرده و از شرکتهای با سابقه و خوشنام طراحی سایت در اصفهان است. از جمله خدمات امین رایان، طراحی سایت، سئو، پشتیبانی فنی سایت و محتوایی سایت، پشتیبانی لینکدین و سامانه ارسال انبوه پیامک میباشد.";
|
||||
|
||||
const navigation = [
|
||||
{
|
||||
title: "درباره ما",
|
||||
href: "/about",
|
||||
title: "صفحات",
|
||||
links: [
|
||||
{ name: "صفحه اصلی", href: "/" },
|
||||
{ name: "خدمات", href: "/#services" },
|
||||
{ name: "نقشه راه", href: "/#roadmap" },
|
||||
{ name: "وبلاگ", href: "/#blog" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "دانشنامه",
|
||||
href: "/blog",
|
||||
},
|
||||
{
|
||||
title: "راه حلها",
|
||||
href: "/solutions",
|
||||
title: "خدمات",
|
||||
links: [
|
||||
{ name: "طراحی سایت در اصفهان", href: "#" },
|
||||
{ name: "سئو در اصفهان", href: "#" },
|
||||
{ name: "پشتیبانی سایت در اصفهان", href: "#" },
|
||||
{ name: "ارسال انبوه در اصفهان", href: "#" },
|
||||
{ name: "پشتیبانی لینکدین", href: "/#services" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function FooterSection({
|
||||
releases,
|
||||
}: {
|
||||
releases: ChangelogRelease[];
|
||||
}) {
|
||||
return (
|
||||
<footer className="py-16 md:py-16 bg-muted border-t">
|
||||
<div className="mx-auto max-w-5xl px-6 flex items-center flex-col">
|
||||
<Logo hasCaption className="mx-auto" />
|
||||
const contactItems = [
|
||||
{
|
||||
label: "آدرس",
|
||||
value: "اصفهان، خیابان چهارباغ عباسی",
|
||||
},
|
||||
{
|
||||
label: "تلفن ثابت",
|
||||
value: "03132208628",
|
||||
href: "tel:+983132208628",
|
||||
},
|
||||
{
|
||||
label: "تلفن",
|
||||
value: "09135502791",
|
||||
href: "tel:+989135502791",
|
||||
},
|
||||
{
|
||||
label: "ایمیل",
|
||||
value: "info@aragrp.com",
|
||||
href: "mailto:info@aragrp.com",
|
||||
},
|
||||
] as const;
|
||||
|
||||
<div className={cn("my-4 flex flex-wrap justify-center gap-6", type.link)}>
|
||||
{links.map((link, index) => (
|
||||
<Link
|
||||
key={index}
|
||||
href={link.href}
|
||||
className="text-muted-foreground hover:text-accent transition-colors duration-150"
|
||||
>
|
||||
<span>{link.title}</span>
|
||||
</Link>
|
||||
))}
|
||||
const socialLinks = [
|
||||
{ icon: Instagram, href: "https://instagram.com", label: "اینستاگرام" },
|
||||
{ icon: Linkedin, href: "https://linkedin.com", label: "لینکدین" },
|
||||
{ icon: Twitter, href: "https://x.com", label: "ایکس" },
|
||||
{ icon: Facebook, href: "https://facebook.com", label: "فیسبوک" },
|
||||
];
|
||||
|
||||
interface FooterSectionProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function FooterSection({ className }: FooterSectionProps) {
|
||||
return (
|
||||
<footer
|
||||
className={cn(
|
||||
"section-shell border-t border-surface bg-section-canvas py-10 md:py-12",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Container className="section-content">
|
||||
<div className="mb-8 border-b border-surface pb-8">
|
||||
<div className="grid grid-cols-1 gap-10 lg:grid-cols-2">
|
||||
<div>
|
||||
<h3 className={cn(type.h3, "mb-2")}>در ارتباط بمانید</h3>
|
||||
<p className={cn(type["body-sm"], "max-w-md text-muted-foreground")}>
|
||||
برای دریافت تازهترین نکات طراحی سایت، سئو و رشد آنلاین، در
|
||||
خبرنامه ما عضو شوید.
|
||||
</p>
|
||||
</div>
|
||||
<form className="flex flex-col gap-3 sm:flex-row">
|
||||
<div className="relative grow">
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="آدرس ایمیل شما"
|
||||
className="h-[var(--btn-height-lg)]"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" size="lg" className="w-full sm:w-auto">
|
||||
عضویت
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<span className={cn(type.link, "block text-center")}>
|
||||
© 1404 فیپ، تمامی حقوق محفوظ است
|
||||
</span>
|
||||
<ChangelogPanel releases={releases} />
|
||||
</div>
|
||||
|
||||
<nav className="grid grid-cols-1 gap-x-8 gap-y-10 border-b border-surface py-8 sm:grid-cols-2 lg:grid-cols-4 lg:gap-x-10 lg:py-10">
|
||||
<div className="sm:col-span-2 pe-0 sm:pe-6 lg:col-span-1 lg:pe-8">
|
||||
<h3 className={cn(type.h4, "mb-5")}>درباره ما</h3>
|
||||
<p
|
||||
className={cn(
|
||||
type["body-sm"],
|
||||
"text-justify text-muted-foreground [text-align-last:start]",
|
||||
)}
|
||||
>
|
||||
{aboutText}
|
||||
</p>
|
||||
</div>
|
||||
{navigation.map((section) => (
|
||||
<div key={section.title} className="text-start">
|
||||
<h3 className={cn(type.h4, "mb-5")}>{section.title}</h3>
|
||||
<ul className="m-0 list-none space-y-4 p-0">
|
||||
{section.links.map((link) => (
|
||||
<li key={link.name}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className={cn(
|
||||
type.link,
|
||||
"block w-fit transition-colors duration-200 hover:text-primary",
|
||||
)}
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
<div className="text-start">
|
||||
<h3 className={cn(type.h4, "mb-5")}>ارتباط با ما</h3>
|
||||
<ul className="m-0 list-none space-y-4 p-0">
|
||||
{contactItems.map((item) => (
|
||||
<li key={item.label} className={cn(type["body-sm"], "text-muted-foreground")}>
|
||||
<span className="text-foreground">{item.label}</span>
|
||||
{" : "}
|
||||
{"href" in item && item.href ? (
|
||||
<a
|
||||
href={item.href}
|
||||
className={cn(
|
||||
type.link,
|
||||
"inline transition-colors duration-200 hover:text-primary",
|
||||
)}
|
||||
dir="ltr"
|
||||
>
|
||||
{item.value}
|
||||
</a>
|
||||
) : (
|
||||
<span>{item.value}</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="mx-auto mt-4 py-5">
|
||||
<div className="flex flex-col items-center justify-between gap-5 text-center sm:flex-row sm:gap-6 sm:text-start">
|
||||
<Logo variant="text" uniColor />
|
||||
<p className={cn(type["body-sm"], "font-medium text-muted-foreground")}>
|
||||
© ۱۴۰۵ امین رایان، تمامی حقوق محفوظ است
|
||||
</p>
|
||||
<div className="flex items-center gap-6">
|
||||
{socialLinks.map((link) => (
|
||||
<a
|
||||
aria-label={link.label}
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-muted-foreground transition-colors duration-200 hover:text-primary"
|
||||
>
|
||||
<link.icon
|
||||
size={20}
|
||||
className="transition-transform hover:scale-110"
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
+428
-132
@@ -1,145 +1,441 @@
|
||||
'use client'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { Logo } from '@/components/logo'
|
||||
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'
|
||||
"use client";
|
||||
|
||||
const menuItems = [
|
||||
{ name: 'صفحه اصلی', href: '/' },
|
||||
{ name: 'دانشنامه', href: '/blog' },
|
||||
{ name: 'راه حلها', href: '/solutions' },
|
||||
{ name: 'مشاوره سازمانی', href: '/consulting' },
|
||||
// { name: 'قیمتگذاری', href: '/pricing' },
|
||||
// { name: 'آزمایشگاه فیپ', href: '/playground' },
|
||||
{ name: 'درباره ما', href: '/about' },
|
||||
]
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Logo } from "@/components/logo";
|
||||
import { Menu } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import {
|
||||
MegaMenuMobileCards,
|
||||
serviceMegaItems,
|
||||
ServicesMegaMenu,
|
||||
} from "@/components/nav-mega-menu";
|
||||
import {
|
||||
Sheet,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import React from "react";
|
||||
import { Container } from "@/components/container";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { type } from "@/lib/typography";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
|
||||
/** Outer gutter — matches hero image wrapper (`px-6 md:px-2`). */
|
||||
const siteGutterClass = 'px-6 md:px-2 lg:px-6'
|
||||
type NavLeaf = {
|
||||
name: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
/** 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'
|
||||
type NavEntry =
|
||||
| ({ kind: "link" } & NavLeaf)
|
||||
| { kind: "mega"; name: string };
|
||||
|
||||
/**
|
||||
* Top-level nav (RTL reading order, right → left):
|
||||
* صفحه اصلی → خدمات → نمونه کارها → درباره ما → مجله → تماس با ما
|
||||
*/
|
||||
/** Experimental: no real routes yet — keep UI, avoid 404s. */
|
||||
const navEntries: NavEntry[] = [
|
||||
{ kind: "link", name: "صفحه اصلی", href: "/" },
|
||||
{ kind: "mega", name: "خدمات" },
|
||||
{ kind: "link", name: "نمونه کارها", href: "#" },
|
||||
{ kind: "link", name: "درباره ما", href: "#" },
|
||||
{ kind: "link", name: "مجله", href: "#" },
|
||||
{ kind: "link", name: "تماس با ما", href: "#contact" },
|
||||
];
|
||||
|
||||
const CTA = {
|
||||
name: "درخواست مشاوره",
|
||||
href: "#contact",
|
||||
} as const;
|
||||
|
||||
const ENGLISH = {
|
||||
name: "English",
|
||||
href: "#",
|
||||
} as const;
|
||||
|
||||
/** Scroll threshold before the floating glass shell engages. */
|
||||
const SCROLL_THRESHOLD = 24;
|
||||
|
||||
/** Animate only the floating shell chrome — never content size or vertical layout. */
|
||||
const HEADER_SHELL_EASE =
|
||||
"transition-[padding,border-radius,background-color,border-color,box-shadow,backdrop-filter,-webkit-backdrop-filter,height] duration-300 ease-out";
|
||||
|
||||
/** Dim siblings on nav hover; hovered + active routes stay full strength. */
|
||||
const navItemInteractionClass = cn(
|
||||
"transition-[opacity,background-color,color] duration-300 ease-out",
|
||||
"text-foreground opacity-100",
|
||||
"group-hover/nav:opacity-40",
|
||||
"hover:opacity-100",
|
||||
"group-hover/nav:hover:opacity-100"
|
||||
);
|
||||
|
||||
const navItemActiveClass = cn(
|
||||
"bg-muted/60 text-foreground group-hover/nav:opacity-100",
|
||||
"hover:bg-muted/70"
|
||||
);
|
||||
|
||||
const navTriggerClass = cn(
|
||||
"group relative inline-flex h-auto items-center gap-1 whitespace-nowrap",
|
||||
type.nav,
|
||||
navItemInteractionClass,
|
||||
"rounded-md bg-transparent px-2.5 py-1.5 shadow-none",
|
||||
"hover:bg-muted/50",
|
||||
"focus-visible:bg-muted/50 focus-visible:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60 focus-visible:ring-offset-2",
|
||||
"data-[state=open]:bg-muted/60 data-[state=open]:text-foreground",
|
||||
"data-[state=open]:opacity-100 group-hover/nav:data-[state=open]:opacity-100"
|
||||
);
|
||||
|
||||
function isLinkActive(pathname: string, href: string): boolean {
|
||||
if (href === '/') return pathname === '/'
|
||||
return pathname === href || pathname.startsWith(href + '/')
|
||||
if (href === "/") return pathname === "/";
|
||||
if (href.startsWith("/#")) return pathname === "/";
|
||||
return pathname === href || pathname.startsWith(href + "/");
|
||||
}
|
||||
|
||||
function isGroupActive(pathname: string): boolean {
|
||||
return serviceMegaItems.some((item) => isLinkActive(pathname, item.href));
|
||||
}
|
||||
|
||||
function NavLink({
|
||||
href,
|
||||
name,
|
||||
active,
|
||||
className,
|
||||
}: {
|
||||
href: string;
|
||||
name: string;
|
||||
active: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
aria-current={active ? "page" : undefined}
|
||||
className={cn(
|
||||
"relative inline-flex items-center whitespace-nowrap rounded-md px-2 py-1.5",
|
||||
type.nav,
|
||||
navItemInteractionClass,
|
||||
"hover:bg-muted/50",
|
||||
active && navItemActiveClass,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function DesktopNav({ pathname }: { pathname: string }) {
|
||||
return (
|
||||
<ul className={cn("group/nav flex list-none items-center justify-start gap-x-1", type.nav)}>
|
||||
{navEntries.map((entry) => {
|
||||
if (entry.kind === "link") {
|
||||
const active = isLinkActive(pathname, entry.href);
|
||||
return (
|
||||
<li key={entry.href}>
|
||||
<NavLink
|
||||
href={entry.href}
|
||||
name={entry.name}
|
||||
active={active}
|
||||
className="px-2.5"
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={entry.name}>
|
||||
<ServicesMegaMenu
|
||||
label={entry.name}
|
||||
pathname={pathname}
|
||||
active={isGroupActive(pathname)}
|
||||
triggerClassName={cn(
|
||||
navTriggerClass,
|
||||
isGroupActive(pathname) && navItemActiveClass
|
||||
)}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
const mobileNavRowClass = cn(
|
||||
"flex w-full items-center rounded-md px-3 py-1.5",
|
||||
"text-muted-foreground/90 transition-[color,background-color,opacity]",
|
||||
"duration-200 ease-out",
|
||||
"hover:bg-muted/60 hover:text-foreground"
|
||||
);
|
||||
|
||||
function MobileNavList({
|
||||
pathname,
|
||||
menuOpen,
|
||||
onNavigate,
|
||||
}: {
|
||||
pathname: string;
|
||||
menuOpen: boolean;
|
||||
onNavigate: () => void;
|
||||
}) {
|
||||
let animIndex = 0;
|
||||
|
||||
return (
|
||||
<ul className={cn("flex flex-col gap-0", type["nav-mobile"])}>
|
||||
{navEntries.map((entry) => {
|
||||
if (entry.kind === "link") {
|
||||
const index = animIndex++;
|
||||
const active = isLinkActive(pathname, entry.href);
|
||||
return (
|
||||
<li
|
||||
key={entry.href}
|
||||
className={cn(
|
||||
"header-nav-item translate-y-1.5 opacity-0",
|
||||
menuOpen && "animate-[header-nav-in_320ms_ease_forwards]"
|
||||
)}
|
||||
style={{
|
||||
animationDelay: menuOpen ? `${80 + index * 40}ms` : undefined,
|
||||
}}
|
||||
>
|
||||
<SheetClose asChild>
|
||||
<Link
|
||||
href={entry.href}
|
||||
className={cn(
|
||||
mobileNavRowClass,
|
||||
active && "bg-muted/50 text-foreground"
|
||||
)}
|
||||
>
|
||||
{entry.name}
|
||||
</Link>
|
||||
</SheetClose>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
const index = animIndex++;
|
||||
const groupActive = isGroupActive(pathname);
|
||||
return (
|
||||
<li
|
||||
key={entry.name}
|
||||
className={cn(
|
||||
"header-nav-item translate-y-1.5 opacity-0",
|
||||
menuOpen && "animate-[header-nav-in_320ms_ease_forwards]"
|
||||
)}
|
||||
style={{
|
||||
animationDelay: menuOpen ? `${80 + index * 40}ms` : undefined,
|
||||
}}
|
||||
>
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value={entry.name} className="border-none">
|
||||
<AccordionTrigger
|
||||
className={cn(
|
||||
type["nav-mobile"],
|
||||
"rounded-md px-3 py-1.5 hover:no-underline",
|
||||
"text-muted-foreground/90 hover:bg-muted/60 hover:text-foreground",
|
||||
groupActive && "text-foreground"
|
||||
)}
|
||||
>
|
||||
<span className="flex items-center gap-1.5">
|
||||
{entry.name}
|
||||
{!groupActive ? null : (
|
||||
<span className="size-1.5 rounded-full bg-primary" />
|
||||
)}
|
||||
</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-1 ps-1">
|
||||
<MegaMenuMobileCards
|
||||
items={serviceMegaItems}
|
||||
pathname={pathname}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
<li
|
||||
className={cn(
|
||||
"header-nav-item translate-y-1.5 opacity-0",
|
||||
menuOpen && "animate-[header-nav-in_320ms_ease_forwards]"
|
||||
)}
|
||||
style={{
|
||||
animationDelay: menuOpen ? `${80 + animIndex * 40}ms` : undefined,
|
||||
}}
|
||||
>
|
||||
<SheetClose asChild>
|
||||
<Link
|
||||
href={ENGLISH.href}
|
||||
lang="en"
|
||||
className={mobileNavRowClass}
|
||||
>
|
||||
{ENGLISH.name}
|
||||
</Link>
|
||||
</SheetClose>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export const HeroHeader = () => {
|
||||
const pathname = usePathname()
|
||||
const [menuState, setMenuState] = React.useState(false)
|
||||
const [isScrolled, setIsScrolled] = React.useState(false)
|
||||
const pathname = usePathname();
|
||||
const [menuOpen, setMenuOpen] = React.useState(false);
|
||||
const [isScrolled, setIsScrolled] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 50)
|
||||
}
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
return () => window.removeEventListener('scroll', handleScroll)
|
||||
}, [])
|
||||
React.useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > SCROLL_THRESHOLD);
|
||||
};
|
||||
handleScroll();
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
document.body.style.overflow = menuState ? 'hidden' : ''
|
||||
return () => {
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
}, [menuState])
|
||||
const topLevelCount = navEntries.length;
|
||||
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
"pointer-events-none fixed inset-x-0 top-0 z-50 w-full",
|
||||
"transition-[padding] duration-300 ease-out",
|
||||
isScrolled
|
||||
? "pt-[calc(env(safe-area-inset-top,0px)+0.5rem)]"
|
||||
: "pt-[env(safe-area-inset-top,0px)]"
|
||||
)}
|
||||
data-scrolled={isScrolled ? "" : undefined}
|
||||
>
|
||||
{/* Same max-width + gutters as every page section — glass edges share the content grid. */}
|
||||
<Container className="pointer-events-auto">
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex w-full items-center justify-between gap-4 md:gap-6 lg:gap-8",
|
||||
isScrolled ? "h-14 md:h-16" : "h-16 md:h-20",
|
||||
"border bg-transparent shadow-none backdrop-blur-none",
|
||||
HEADER_SHELL_EASE,
|
||||
isScrolled
|
||||
? [
|
||||
// Horizontal inset only — no vertical shift of logo / nav / CTA
|
||||
"rounded-2xl border-border/50 bg-background/75 px-4 md:px-6",
|
||||
"shadow-sm backdrop-blur-xl backdrop-saturate-150",
|
||||
"supports-[backdrop-filter]:bg-background/65",
|
||||
]
|
||||
: // Flush with Container gutters — same vertical guides as page content
|
||||
"rounded-none border-transparent px-0"
|
||||
)}
|
||||
>
|
||||
{/* Logo + nav sit together so RTL reading starts: لوگو → صفحه اصلی → … */}
|
||||
<div className="relative z-10 flex min-w-0 flex-1 items-center gap-5 lg:gap-8">
|
||||
<Logo
|
||||
variant="mark"
|
||||
priority
|
||||
className="shrink-0 transition-opacity duration-300 ease-out hover:opacity-80"
|
||||
/>
|
||||
|
||||
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="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 (
|
||||
<li key={index}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'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="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>
|
||||
aria-label="ناوبری اصلی"
|
||||
className="hidden min-w-0 items-center xl:flex"
|
||||
>
|
||||
<DesktopNav pathname={pathname ?? ""} />
|
||||
</nav>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex shrink-0 items-center gap-2 sm:gap-3">
|
||||
<Link
|
||||
href={ENGLISH.href}
|
||||
className={cn(
|
||||
type.nav,
|
||||
"hidden px-2.5 py-1.5 text-muted-foreground/80",
|
||||
"transition-colors duration-300 ease-out hover:text-foreground xl:inline-flex"
|
||||
)}
|
||||
lang="en"
|
||||
>
|
||||
{ENGLISH.name}
|
||||
</Link>
|
||||
|
||||
<ThemeToggle />
|
||||
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
className="hidden hover:-translate-y-px xl:inline-flex"
|
||||
>
|
||||
<Link href={CTA.href}>{CTA.name}</Link>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setMenuOpen(true)}
|
||||
aria-expanded={menuOpen}
|
||||
aria-controls="mobile-nav"
|
||||
aria-label="باز کردن منو"
|
||||
className="text-muted-foreground hover:text-foreground xl:hidden"
|
||||
>
|
||||
<Menu aria-hidden className="size-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
<Sheet open={menuOpen} onOpenChange={setMenuOpen}>
|
||||
<SheetContent
|
||||
id="mobile-nav"
|
||||
side="right"
|
||||
closePosition="start"
|
||||
className={cn(
|
||||
"pointer-events-auto w-[min(100%,20rem)] gap-0 border-border/50 bg-background/95 p-0",
|
||||
"backdrop-blur-xl backdrop-saturate-150 shadow-xl",
|
||||
"data-[state=open]:duration-300 data-[state=closed]:duration-300",
|
||||
"ease-out"
|
||||
)}
|
||||
>
|
||||
<SheetHeader className="border-b border-border/40 px-6 py-4">
|
||||
<div className="flex items-center justify-between gap-3 pe-8">
|
||||
<SheetTitle className="sr-only">منوی اصلی</SheetTitle>
|
||||
<Logo variant="mark" />
|
||||
</div>
|
||||
</SheetHeader>
|
||||
|
||||
<nav
|
||||
aria-label="ناوبری موبایل"
|
||||
className="flex flex-1 flex-col px-3 py-4"
|
||||
>
|
||||
<MobileNavList
|
||||
pathname={pathname ?? ""}
|
||||
menuOpen={menuOpen}
|
||||
onNavigate={() => setMenuOpen(false)}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"header-nav-item mt-auto border-t border-border/40 px-3 pt-4 pb-[max(1rem,env(safe-area-inset-bottom))]",
|
||||
"translate-y-1.5 opacity-0",
|
||||
menuOpen && "animate-[header-nav-in_320ms_ease_forwards]"
|
||||
)}
|
||||
style={{
|
||||
animationDelay: menuOpen
|
||||
? `${80 + (topLevelCount + 1) * 40}ms`
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<SheetClose asChild>
|
||||
<Button asChild className="w-full hover:-translate-y-px">
|
||||
<Link href={CTA.href}>{CTA.name}</Link>
|
||||
</Button>
|
||||
</SheetClose>
|
||||
</div>
|
||||
</nav>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { Figma, GalleryVerticalEnd, Signature } from "lucide-react";
|
||||
import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
|
||||
|
||||
import { Container } from "@/components/container";
|
||||
import { layout } from "@/lib/layout";
|
||||
import { type, typeSpacing } from "@/lib/typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type MaskType = "type-1" | "type-2" | "type-3" | "type-4";
|
||||
|
||||
const images = [
|
||||
{
|
||||
src: "/assets/main-pic/1.jpg",
|
||||
alt: "نمونه فضای کاری و طراحی امین رایان",
|
||||
},
|
||||
{
|
||||
src: "/assets/main-pic/intelligent-workflow-control-center.jpg",
|
||||
alt: "نمونه پروژه طراحی سایت امین رایان",
|
||||
},
|
||||
{
|
||||
src: "/assets/main-pic/4section/Systems Integration.jpg",
|
||||
alt: "نمونه یکپارچهسازی و طراحی وب",
|
||||
},
|
||||
];
|
||||
|
||||
interface Hero213Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Hero213({ className }: Hero213Props) {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setCurrentIndex((prevIndex) => (prevIndex + 1) % images.length);
|
||||
}, 2500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [currentIndex]);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
"section-shell bg-gradient-hero",
|
||||
layout.sectionTopWithHeader,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="section-decor" aria-hidden>
|
||||
<div className="glow-orb glow-orb--primary-md absolute end-[-10%] top-0 size-[min(36rem,90vw)]" />
|
||||
<div className="glow-orb glow-orb--secondary-soft absolute -start-[8%] bottom-[12%] size-[min(28rem,70vw)]" />
|
||||
</div>
|
||||
<Container className="section-content">
|
||||
<div
|
||||
className={cn(
|
||||
"mx-auto flex max-w-2xl flex-col items-center justify-center",
|
||||
typeSpacing.heroStack
|
||||
)}
|
||||
>
|
||||
<h1 className={cn(type.display, "text-center text-foreground")}>
|
||||
امین رایان؛ امینِ رویای آنلاین
|
||||
</h1>
|
||||
<p
|
||||
className={cn(
|
||||
type["body-lg"],
|
||||
"max-w-xl px-4 text-center text-muted-foreground sm:px-10"
|
||||
)}
|
||||
>
|
||||
طراحی سایت، سئو و پشتیبانی دیجیتال برای کسبوکارهایی که میخواهند
|
||||
در اصفهان و فراتر از آن دیده شوند.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative mt-12 w-full md:mt-16">
|
||||
<div className="relative mx-auto w-full sm:w-[94%] lg:w-[92%]">
|
||||
<div className="absolute left-1/2 top-0 z-20 flex w-fit -translate-x-1/2 -translate-y-1/2 items-center justify-center gap-6 rounded-full border border-border/60 bg-muted/80 px-4 py-2 text-muted-foreground shadow-sm backdrop-blur-sm transition-colors duration-200 hover:border-primary/30 hover:text-foreground sm:gap-8 md:px-6 md:py-2.5 lg:px-8 lg:py-3">
|
||||
<GalleryVerticalEnd className="size-4 opacity-50 transition-opacity duration-200 hover:opacity-100 lg:size-5" />
|
||||
<Signature className="size-4 opacity-50 transition-opacity duration-200 hover:opacity-100 lg:size-5" />
|
||||
<Figma className="size-4 opacity-50 transition-opacity duration-200 hover:opacity-100 lg:size-5" />
|
||||
</div>
|
||||
|
||||
<MaskedDiv maskType="type-3">
|
||||
<AnimatePresence mode="popLayout">
|
||||
<motion.img
|
||||
key={currentIndex}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
src={images[currentIndex].src}
|
||||
alt={images[currentIndex].alt}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 1.5 }}
|
||||
/>
|
||||
</AnimatePresence>
|
||||
</MaskedDiv>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
interface SvgPath {
|
||||
path: string;
|
||||
height: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
interface MaskedDivProps {
|
||||
children: ReactNode;
|
||||
maskType?: MaskType;
|
||||
className?: string;
|
||||
backgroundColor?: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const svgPaths: Record<MaskType, SvgPath> = {
|
||||
"type-1": {
|
||||
path: "M0.928955 40.9769C0.928955 18.9149 18.7917 1.01844 40.8536 0.976903L289.97 0.507853C308.413 0.473128 323.521 15.1483 324.022 33.5845L324.886 65.4007C325.955 104.745 358.022 136.159 397.38 136.417L432.98 136.65C447.818 136.748 459.797 148.799 459.803 163.637L459.982 550.982C459.992 573.08 442.08 591 419.982 591H40.9289C18.8376 591 0.928955 573.091 0.928955 551V40.9769Z",
|
||||
height: 591,
|
||||
width: 460,
|
||||
},
|
||||
"type-2": {
|
||||
path: "M0.811768 77.2118C0.811768 60.4225 14.4222 46.8121 31.2115 46.8121H180.95C192.496 46.8121 201.855 37.4527 201.855 25.9073V25.9073C201.855 11.9565 213.164 0.647217 227.115 0.647217H529.273C548.014 0.647217 563.206 15.8395 563.206 34.5802V34.5802C563.206 50.0897 575.779 62.6626 591.289 62.6626H820.388C837.177 62.6626 850.787 76.273 850.787 93.0623V350.953C850.787 367.742 837.177 381.353 820.388 381.353H366.165C349.852 381.353 336.627 368.128 336.627 351.814V351.814C336.627 335.501 323.402 322.276 307.089 322.276H31.2114C14.4222 322.276 0.811768 308.666 0.811768 291.876V77.2118Z",
|
||||
height: 381,
|
||||
width: 850,
|
||||
},
|
||||
"type-3": {
|
||||
path: "M0.680664 112.659C0.680664 50.805 50.823 0.662672 112.677 0.662672H413.07C456.315 0.662672 497.495 19.1588 526.221 51.4846V51.4846C554.948 83.8104 596.128 102.307 639.373 102.307H711.793C752.427 102.307 790.787 83.5522 815.744 51.4846V51.4846C840.7 19.417 879.06 0.662644 919.695 0.662597L1225.01 0.66224C1286.86 0.662168 1337.01 50.8046 1337.01 112.658V652.815C1337.01 714.668 1286.86 764.811 1225.01 764.811L670 764.811H335.34H278.376C217.423 764.811 168.01 715.399 168.01 654.446V626.747C168.01 580.208 130.283 542.48 83.7437 542.48V542.48C37.8692 542.48 0.680664 505.292 0.680664 459.417V382.737V112.659Z",
|
||||
height: 765,
|
||||
width: 1340,
|
||||
},
|
||||
"type-4": {
|
||||
path: "M0.811768 34.5451C0.811768 15.7441 16.053 0.502808 34.8541 0.502808H816.745C835.546 0.502808 850.787 15.7441 850.787 34.5452V242.977C850.787 261.778 835.546 277.019 816.745 277.019H638.293H550.537C527.035 277.019 504.789 266.407 490.001 248.141L486.211 243.46C453.263 202.765 390.688 204.378 359.881 246.717V246.717C346.027 265.756 323.901 277.019 300.355 277.019H213.306H34.8541C16.0531 277.019 0.811768 261.778 0.811768 242.977V34.5451Z",
|
||||
height: 278,
|
||||
width: 851,
|
||||
},
|
||||
};
|
||||
|
||||
function MaskedDiv({
|
||||
children,
|
||||
maskType = "type-1",
|
||||
className = "",
|
||||
backgroundColor = "transparent",
|
||||
size = 1,
|
||||
}: MaskedDivProps) {
|
||||
const selectedMask = svgPaths[maskType];
|
||||
|
||||
const svgString = `data:image/svg+xml,%3Csvg width='${selectedMask.width}' height='${selectedMask.height}' viewBox='0 0 ${selectedMask.width} ${selectedMask.height}' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fillRule='evenodd' clipRule='evenodd' d='${selectedMask.path}' fill='%23D9D9D9'/%3E%3C/svg%3E%0A`;
|
||||
|
||||
const containerStyle: CSSProperties = {
|
||||
aspectRatio: `${selectedMask.width}/${selectedMask.height}`,
|
||||
backgroundColor,
|
||||
maskImage: `url("${svgString}")`,
|
||||
WebkitMaskImage: `url("${svgString}")`,
|
||||
maskRepeat: "no-repeat",
|
||||
WebkitMaskRepeat: "no-repeat",
|
||||
maskSize: "contain",
|
||||
WebkitMaskSize: "contain",
|
||||
width: `${size * 100}%`,
|
||||
maxWidth: "100%",
|
||||
margin: "0 auto",
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("relative overflow-hidden", className)} style={containerStyle}>
|
||||
<div className="absolute inset-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Container } from "@/components/container";
|
||||
import { MotionReveal, MotionRevealItem } from "@/components/motion-reveal";
|
||||
import { BorderBeam } from "@/components/ui/border-beam";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { layout } from "@/lib/layout";
|
||||
import { type } from "@/lib/typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Hero50Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Hero50({ className }: Hero50Props) {
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
"section-shell bg-section-canvas",
|
||||
layout.scrollMargin,
|
||||
layout.sectionY,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Container className="section-content">
|
||||
<div className="flex flex-col gap-8 lg:flex-row-reverse lg:items-center lg:gap-12">
|
||||
<MotionReveal stagger className="min-w-0 flex-1">
|
||||
<MotionRevealItem>
|
||||
<h2 className={cn(type.h2, "text-foreground")}>
|
||||
شرکت امین رایان اسپادان
|
||||
</h2>
|
||||
</MotionRevealItem>
|
||||
<MotionRevealItem>
|
||||
<p className={cn(type["section-intro"], "mt-4")}>
|
||||
شرکت امین رایان، بهترین شرکت طراحی سایت در اصفهان، با ارائه خدمات حرفهای طراحی سایت در اصفهان، سئو و پشتیبانی سایت، پشتیبانی لینکدین و ارسال انبوه پیامک در اصفهان، همراه مطمئن کسبوکار شماست. امین رایان با هدف حمایت از تمامی کسبوکارها فعالیت میکند و برای ما فرقی نمیکند که کسبوکار شما تازه راهاندازی شده باشد یا یک مجموعه وسیع و پیشرفته. با همکاری با امین رایان، همیشه بهروز و حرفهای باقی بمانید و از جدیدترین امکانات دیجیتال بهرهمند شوید.
|
||||
</p>
|
||||
</MotionRevealItem>
|
||||
<MotionRevealItem>
|
||||
<div className="mt-6 flex flex-col gap-4 sm:flex-row sm:justify-start">
|
||||
<Button size="lg" className="w-full sm:w-auto" asChild>
|
||||
<a href="#contact">تماس با ما</a>
|
||||
</Button>
|
||||
</div>
|
||||
</MotionRevealItem>
|
||||
</MotionReveal>
|
||||
<MotionReveal className="min-w-0 w-full lg:w-1/2">
|
||||
<div className="relative">
|
||||
<div className="local-highlight local-highlight--soft" aria-hidden />
|
||||
<div className="media-frame relative z-[1] rounded-xl">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src="/assets/main-pic/about-us.jpg"
|
||||
alt="تصویر امین رایان"
|
||||
className="max-h-[22rem] w-full rounded-[inherit] object-cover sm:max-h-[28rem] lg:max-h-[580px]"
|
||||
/>
|
||||
<BorderBeam
|
||||
duration={6}
|
||||
size={200}
|
||||
borderWidth={1}
|
||||
colorFrom="#29A19C"
|
||||
colorTo="#044343"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</MotionReveal>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,711 @@
|
||||
"use client";
|
||||
|
||||
import { motion, useReducedMotion } from "motion/react";
|
||||
import { Check, ChevronLeft, LayoutTemplate, Lock } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Container } from "@/components/container";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { blockEase } from "@/lib/block-motion";
|
||||
import { layout } from "@/lib/layout";
|
||||
import { type } from "@/lib/typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/** Soft page-load entrance — opacity + gentle rise. */
|
||||
const heroFadeEase = blockEase;
|
||||
|
||||
const heroContainerVariants = {
|
||||
hidden: {},
|
||||
visible: {
|
||||
transition: {
|
||||
staggerChildren: 0.14,
|
||||
delayChildren: 0.06,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const heroColumnVariants = {
|
||||
hidden: {},
|
||||
visible: {
|
||||
transition: {
|
||||
staggerChildren: 0.09,
|
||||
delayChildren: 0.04,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const heroItemVariants = {
|
||||
hidden: { opacity: 0, y: 14 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.7, ease: heroFadeEase },
|
||||
},
|
||||
};
|
||||
|
||||
const heroMockupVariants = {
|
||||
hidden: { opacity: 0, y: 20, scale: 0.985 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
transition: { duration: 0.9, ease: heroFadeEase },
|
||||
},
|
||||
};
|
||||
|
||||
/** Very soft float — long cycle, tiny travel, custom ease. */
|
||||
const floatTransition = {
|
||||
duration: 11,
|
||||
repeat: Infinity,
|
||||
ease: [0.45, 0.05, 0.55, 0.95] as const,
|
||||
};
|
||||
|
||||
const trustAvatars = [
|
||||
{ initials: "سا", tone: "bg-primary/15 text-primary" },
|
||||
{ initials: "ام", tone: "bg-muted text-foreground" },
|
||||
{ initials: "نر", tone: "bg-primary/10 text-primary" },
|
||||
{ initials: "مد", tone: "bg-muted text-muted-foreground" },
|
||||
] as const;
|
||||
|
||||
const screenBlocks = [
|
||||
{ label: "طراحی", w: "w-[42%]" },
|
||||
{ label: "سئو", w: "w-[34%]" },
|
||||
{ label: "پشتیبانی", w: "w-[48%]" },
|
||||
] as const;
|
||||
|
||||
/** Soft ease for in-screen design loop. */
|
||||
const screenLoopEase = [0.45, 0.05, 0.55, 0.95] as const;
|
||||
|
||||
/** Extra-soft ease for skeleton text lines — slow in/out, no snap. */
|
||||
const screenLineEase = [0.37, 0, 0.2, 1] as const;
|
||||
|
||||
const screenLineTransition = {
|
||||
duration: 9.5,
|
||||
repeat: Infinity,
|
||||
ease: screenLineEase,
|
||||
times: [0, 0.35, 0.65, 1] as const,
|
||||
};
|
||||
|
||||
interface HeroReuiProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function HeroReui({ className }: HeroReuiProps) {
|
||||
const reduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
"section-shell hero-atmosphere flex min-h-svh flex-col justify-center",
|
||||
/* Clear fixed header; bottom matches sectionY so gap to next block is standard */
|
||||
layout.sectionTopWithHeader,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className="hero-square-pattern" aria-hidden />
|
||||
<Container className="section-content w-full">
|
||||
<motion.div
|
||||
className="grid items-center gap-10 sm:gap-12 lg:grid-cols-[minmax(0,0.92fr)_minmax(0,1.08fr)] lg:gap-16 xl:gap-20"
|
||||
initial={reduceMotion ? false : "hidden"}
|
||||
animate="visible"
|
||||
variants={reduceMotion ? undefined : heroContainerVariants}
|
||||
>
|
||||
{/* Copy */}
|
||||
<motion.div
|
||||
className="mx-auto flex max-w-xl flex-col items-center text-center lg:mx-0 lg:items-start lg:text-start"
|
||||
variants={reduceMotion ? undefined : heroColumnVariants}
|
||||
>
|
||||
<motion.div variants={reduceMotion ? undefined : heroItemVariants}>
|
||||
<Link
|
||||
href="#services"
|
||||
className={cn(
|
||||
type["body-sm"],
|
||||
"group inline-flex max-w-full items-center gap-1.5 rounded-full border border-surface bg-background/70 px-2.5 py-1.5 text-muted-foreground shadow-surface-sm backdrop-blur-sm transition-colors duration-200 hover:border-primary/35 hover:text-foreground sm:gap-2 sm:px-3"
|
||||
)}
|
||||
>
|
||||
<Badge variant="default" className={cn(type.micro, "shrink-0 px-1.5 py-0")}>
|
||||
اصفهان
|
||||
</Badge>
|
||||
<span className="truncate">طراحی سایت و سئو حرفهای</span>
|
||||
<ChevronLeft className="size-3.5 shrink-0 opacity-60 transition-transform duration-200 group-hover:-translate-x-0.5 rtl:group-hover:translate-x-0.5" />
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
<motion.h1
|
||||
variants={reduceMotion ? undefined : heroItemVariants}
|
||||
className={cn(
|
||||
type.display,
|
||||
"mt-6 text-center text-foreground lg:text-start"
|
||||
)}
|
||||
>
|
||||
امین رایان
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
variants={reduceMotion ? undefined : heroItemVariants}
|
||||
className={cn(
|
||||
type["body-lg"],
|
||||
"mt-3 max-w-md text-center font-medium text-foreground/90 lg:text-start"
|
||||
)}
|
||||
>
|
||||
امینِ رویای آنلاین
|
||||
</motion.p>
|
||||
|
||||
<motion.p
|
||||
variants={reduceMotion ? undefined : heroItemVariants}
|
||||
className={cn(
|
||||
type.body,
|
||||
"mt-4 max-w-md text-center text-muted-foreground lg:text-start"
|
||||
)}
|
||||
>
|
||||
از طراحی وبسایت حرفهای تا بهینهسازی موتورهای جستجو و پشتیبانی
|
||||
پایدار؛ همراه مطمئن کسبوکارهای اصفهان و سراسر ایران.
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
variants={reduceMotion ? undefined : heroItemVariants}
|
||||
className="mt-8 flex w-full flex-col gap-3 sm:w-auto sm:flex-row sm:items-center"
|
||||
>
|
||||
<Button size="lg" className="w-full sm:w-auto" asChild>
|
||||
<a href="#contact">
|
||||
درخواست مشاوره
|
||||
<ChevronLeft className="size-4" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button size="lg" variant="secondary" className="w-full sm:w-auto" asChild>
|
||||
<a href="#services">مشاهده خدمات</a>
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={reduceMotion ? undefined : heroItemVariants}
|
||||
className="mt-10 flex items-center gap-3"
|
||||
>
|
||||
<div className="flex -space-x-2 space-x-reverse" aria-hidden>
|
||||
{trustAvatars.map((avatar) => (
|
||||
<span
|
||||
key={avatar.initials}
|
||||
className={cn(
|
||||
type.micro,
|
||||
"flex size-8 items-center justify-center rounded-full border-2 border-background font-semibold",
|
||||
avatar.tone
|
||||
)}
|
||||
>
|
||||
{avatar.initials}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className={cn(type.caption, "max-w-[15rem]")}>
|
||||
مورد اعتماد کسبوکارهایی که رشد آنلاین میخواهند
|
||||
</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Desk + monitor scene */}
|
||||
<motion.div
|
||||
className="relative mx-auto w-full max-w-xl lg:max-w-none"
|
||||
variants={reduceMotion ? undefined : heroMockupVariants}
|
||||
>
|
||||
<div className="relative">
|
||||
<DeskMonitorMockup reduceMotion={!!reduceMotion} />
|
||||
|
||||
{/* Soft status chip — kept minimal beside the desk */}
|
||||
<motion.div
|
||||
className="absolute start-1 top-[10%] z-20 max-w-[calc(100%-1rem)] will-change-transform sm:start-0 sm:max-w-none lg:-start-4"
|
||||
initial={reduceMotion ? false : { opacity: 0, y: 10 }}
|
||||
animate={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: { opacity: 1, y: [0, -14, 0, -8, 0], x: [0, 4, 0, -3, 0] }
|
||||
}
|
||||
transition={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: {
|
||||
opacity: {
|
||||
duration: 0.65,
|
||||
delay: 0.85,
|
||||
ease: heroFadeEase,
|
||||
},
|
||||
y: {
|
||||
...floatTransition,
|
||||
duration: 7.5,
|
||||
delay: 1.1,
|
||||
},
|
||||
x: {
|
||||
...floatTransition,
|
||||
duration: 9,
|
||||
delay: 1.1,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2 rounded-xl border border-surface bg-background/95 px-2.5 py-1.5 shadow-floating backdrop-blur-sm sm:gap-2.5 sm:px-3 sm:py-2">
|
||||
<span className="flex size-5 shrink-0 items-center justify-center rounded-full bg-primary/15 text-primary sm:size-6">
|
||||
<Check className="size-3 sm:size-3.5" strokeWidth={2.5} />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className={cn(type.caption, "font-medium text-foreground")}>
|
||||
در حال طراحی
|
||||
</p>
|
||||
<p className={cn(type.micro, "truncate text-muted-foreground")}>
|
||||
لندینگ فروشگاهی
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="absolute end-1 bottom-[22%] z-20 will-change-transform sm:end-0 lg:-end-3"
|
||||
initial={reduceMotion ? false : { opacity: 0, y: 10 }}
|
||||
animate={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: { opacity: 1, y: [0, 12, 0, 7, 0], x: [0, -4, 0, 3, 0] }
|
||||
}
|
||||
transition={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: {
|
||||
opacity: {
|
||||
duration: 0.65,
|
||||
delay: 1,
|
||||
ease: heroFadeEase,
|
||||
},
|
||||
y: {
|
||||
...floatTransition,
|
||||
duration: 8.5,
|
||||
delay: 1.35,
|
||||
},
|
||||
x: {
|
||||
...floatTransition,
|
||||
duration: 10,
|
||||
delay: 1.35,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 rounded-full border border-surface bg-background/95 px-2 py-1.5 shadow-floating backdrop-blur-sm sm:gap-2 sm:px-2.5">
|
||||
<span className="flex size-5 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground">
|
||||
<LayoutTemplate className="size-3" />
|
||||
</span>
|
||||
<span className={cn(type.caption, "font-medium text-foreground")}>
|
||||
پیشنمایش زنده
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function DeskMonitorMockup({ reduceMotion }: { reduceMotion: boolean }) {
|
||||
return (
|
||||
<div
|
||||
className="relative mx-auto w-full max-w-lg lg:max-w-none"
|
||||
role="img"
|
||||
aria-label="میز کار طراحی وب با مانیتور و پیشنمایش زنده صفحه"
|
||||
>
|
||||
{/* Soft desk glow behind monitor */}
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-[8%] top-[18%] bottom-[18%] rounded-[50%] bg-primary/10 blur-3xl dark:bg-primary/15"
|
||||
aria-hidden
|
||||
/>
|
||||
|
||||
<div className="relative flex flex-col items-center">
|
||||
{/* Monitor + stand float above a fixed desk */}
|
||||
<motion.div
|
||||
className="relative flex w-full flex-col items-center will-change-transform"
|
||||
animate={reduceMotion ? undefined : { y: [0, -14, 0] }}
|
||||
transition={reduceMotion ? undefined : floatTransition}
|
||||
>
|
||||
<div className="relative w-full">
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-hidden rounded-[1.05rem] border border-foreground/15 bg-foreground/90 p-[0.55rem] shadow-hero sm:rounded-[1.25rem] sm:p-2.5",
|
||||
"dark:border-white/10 dark:bg-zinc-900"
|
||||
)}
|
||||
>
|
||||
{/* Camera notch */}
|
||||
<div
|
||||
className="mx-auto mb-1.5 h-1 w-10 rounded-full bg-background/20 sm:mb-2 sm:h-1.5 sm:w-12"
|
||||
aria-hidden
|
||||
/>
|
||||
|
||||
{/* Screen */}
|
||||
<div className="relative overflow-hidden rounded-md bg-background sm:rounded-lg">
|
||||
<div className="aspect-[16/10] sm:aspect-[16/10.5]">
|
||||
<LiveWebsiteScreen reduceMotion={reduceMotion} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Neck */}
|
||||
<div
|
||||
className="h-5 w-14 bg-gradient-to-b from-foreground/55 to-foreground/35 sm:h-6 sm:w-16 dark:from-zinc-600 dark:to-zinc-700"
|
||||
aria-hidden
|
||||
/>
|
||||
|
||||
{/* Stand base */}
|
||||
<div
|
||||
className="h-2.5 w-28 rounded-t-md bg-gradient-to-b from-foreground/40 to-foreground/25 sm:h-3 sm:w-32 dark:from-zinc-600 dark:to-zinc-800"
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
className="h-1.5 w-36 rounded-full bg-foreground/20 sm:w-40 dark:bg-zinc-700/80"
|
||||
aria-hidden
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Desk surface stays grounded */}
|
||||
<div className="relative mt-3 w-full max-w-md" aria-hidden>
|
||||
<div className="h-3 rounded-full bg-gradient-to-b from-muted via-muted/80 to-transparent shadow-surface-sm" />
|
||||
{/* Tiny desk props */}
|
||||
<div className="absolute -top-1 end-[12%] hidden size-6 rounded-full border border-surface bg-primary/15 sm:block" />
|
||||
<div className="absolute -top-0.5 start-[14%] hidden h-2 w-10 rounded-full bg-muted-foreground/20 sm:block" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LiveWebsiteScreen({ reduceMotion }: { reduceMotion: boolean }) {
|
||||
return (
|
||||
<div className="relative flex h-full flex-col bg-background">
|
||||
{/* Browser chrome */}
|
||||
<div className="flex shrink-0 items-center gap-2 border-b border-surface bg-muted/40 px-2 py-1.5 sm:px-2.5 sm:py-2">
|
||||
<div className="flex shrink-0 gap-1" aria-hidden>
|
||||
<span className="size-1.5 rounded-full bg-border sm:size-2" />
|
||||
<span className="size-1.5 rounded-full bg-border sm:size-2" />
|
||||
<span className="size-1.5 rounded-full bg-border sm:size-2" />
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 items-center justify-center gap-1 rounded-md border border-surface bg-background px-2 py-0.5 sm:gap-1.5 sm:px-2.5 sm:py-1">
|
||||
<Lock className="size-2.5 shrink-0 text-muted-foreground sm:size-3" />
|
||||
<span className={cn(type.micro, "truncate text-muted-foreground")}>
|
||||
preview.aminrayan.ir
|
||||
</span>
|
||||
{!reduceMotion && (
|
||||
<motion.span
|
||||
className="ms-0.5 size-1.5 shrink-0 rounded-full bg-primary"
|
||||
aria-hidden
|
||||
animate={{ opacity: [0.35, 1, 0.35] }}
|
||||
transition={{
|
||||
duration: 2.4,
|
||||
repeat: Infinity,
|
||||
ease: screenLoopEase,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mini site canvas */}
|
||||
<div className="relative min-h-0 flex-1 overflow-hidden p-2.5 sm:p-3.5">
|
||||
{/* Soft page wash */}
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 bg-gradient-to-br from-primary/[0.04] via-transparent to-muted/40"
|
||||
aria-hidden
|
||||
/>
|
||||
|
||||
{/* Ambient light sweep across the canvas */}
|
||||
{!reduceMotion && (
|
||||
<motion.div
|
||||
className="pointer-events-none absolute inset-y-0 w-1/3 bg-gradient-to-l from-transparent via-primary/[0.07] to-transparent"
|
||||
aria-hidden
|
||||
animate={{ x: ["120%", "-140%"] }}
|
||||
transition={{
|
||||
duration: 7.5,
|
||||
repeat: Infinity,
|
||||
ease: "linear",
|
||||
repeatDelay: 1.2,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="relative flex h-full flex-col gap-2.5 sm:gap-3">
|
||||
{/* Mini nav */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<motion.span
|
||||
className="size-4 rounded-md bg-primary sm:size-5"
|
||||
animate={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: { scale: [1, 1.08, 1], rotate: [0, -4, 0] }
|
||||
}
|
||||
transition={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: {
|
||||
duration: 5,
|
||||
repeat: Infinity,
|
||||
ease: screenLoopEase,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<span className={cn(type.micro, "font-semibold text-foreground")}>
|
||||
برند شما
|
||||
</span>
|
||||
</div>
|
||||
<div className="hidden items-center gap-1.5 sm:flex" aria-hidden>
|
||||
{[0, 1, 2].map((i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={cn(
|
||||
"relative h-1.5 overflow-hidden rounded-full bg-muted-foreground/25",
|
||||
i === 2 ? "w-6" : "w-8"
|
||||
)}
|
||||
>
|
||||
<motion.span
|
||||
className="absolute inset-0 rounded-full bg-primary/70"
|
||||
animate={
|
||||
reduceMotion ? undefined : { opacity: [0, 1, 0] }
|
||||
}
|
||||
transition={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: {
|
||||
duration: 3.8,
|
||||
repeat: Infinity,
|
||||
ease: screenLoopEase,
|
||||
delay: i * 0.85,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hero copy block — soft breathe via scale (no layout width jumps) */}
|
||||
<div className="mt-0.5 space-y-1.5 sm:mt-1 sm:space-y-2">
|
||||
<motion.div
|
||||
className="h-2.5 w-[72%] origin-right rounded-full bg-foreground/85 sm:h-3"
|
||||
animate={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: {
|
||||
scaleX: [0.86, 1, 0.94, 1],
|
||||
opacity: [0.72, 1, 0.88, 1],
|
||||
}
|
||||
}
|
||||
transition={
|
||||
reduceMotion ? undefined : screenLineTransition
|
||||
}
|
||||
/>
|
||||
<motion.div
|
||||
className="h-1.5 w-[88%] origin-right rounded-full bg-muted-foreground/30 sm:h-2"
|
||||
animate={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: {
|
||||
scaleX: [0.84, 1, 0.92, 1],
|
||||
opacity: [0.4, 0.8, 0.55, 0.8],
|
||||
}
|
||||
}
|
||||
transition={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: { ...screenLineTransition, delay: 0.4 }
|
||||
}
|
||||
/>
|
||||
<motion.div
|
||||
className="h-1.5 w-[64%] origin-right rounded-full bg-muted-foreground/20 sm:h-2"
|
||||
animate={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: {
|
||||
scaleX: [0.82, 1, 0.9, 1],
|
||||
opacity: [0.32, 0.65, 0.45, 0.65],
|
||||
}
|
||||
}
|
||||
transition={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: { ...screenLineTransition, delay: 0.75 }
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* CTA — click target for cursor loop */}
|
||||
<div className="relative mt-1 inline-flex sm:mt-1.5">
|
||||
<motion.div
|
||||
className="relative inline-flex items-center rounded-md bg-primary px-2.5 py-1 text-primary-foreground shadow-surface-sm sm:px-3 sm:py-1.5"
|
||||
animate={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: {
|
||||
scale: [1, 1, 1, 0.94, 1.06, 1, 1],
|
||||
y: [0, -1, 0, 0, 0, 0, 0],
|
||||
}
|
||||
}
|
||||
transition={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: {
|
||||
duration: 10,
|
||||
repeat: Infinity,
|
||||
ease: screenLoopEase,
|
||||
times: [0, 0.18, 0.32, 0.4, 0.48, 0.58, 1],
|
||||
}
|
||||
}
|
||||
>
|
||||
<span className={cn(type.micro, "font-medium")}>شروع پروژه</span>
|
||||
<ChevronLeft className="ms-0.5 size-2.5 opacity-80 sm:size-3" />
|
||||
</motion.div>
|
||||
|
||||
{/* Click ripple */}
|
||||
{!reduceMotion && (
|
||||
<motion.span
|
||||
className="pointer-events-none absolute start-1/2 top-1/2 size-8 -translate-x-1/2 -translate-y-1/2 rounded-full border border-primary/50"
|
||||
aria-hidden
|
||||
animate={{
|
||||
opacity: [0, 0, 0, 0.6, 0, 0, 0],
|
||||
scale: [0.4, 0.4, 0.4, 1.4, 1.8, 1.8, 1.8],
|
||||
}}
|
||||
transition={{
|
||||
duration: 10,
|
||||
repeat: Infinity,
|
||||
ease: screenLoopEase,
|
||||
times: [0, 0.32, 0.38, 0.46, 0.58, 0.7, 1],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Soft progress / build bar */}
|
||||
<div className="h-1 overflow-hidden rounded-full bg-muted/60" aria-hidden>
|
||||
{!reduceMotion ? (
|
||||
<motion.div
|
||||
className="h-full rounded-full bg-primary/70"
|
||||
animate={{ width: ["12%", "78%", "52%", "92%", "12%"] }}
|
||||
transition={{
|
||||
duration: 10,
|
||||
repeat: Infinity,
|
||||
ease: screenLoopEase,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full w-3/4 rounded-full bg-primary/70" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content cards — rise in, highlight in sequence */}
|
||||
<div className="mt-auto grid grid-cols-3 gap-1.5 sm:gap-2">
|
||||
{screenBlocks.map((block, i) => (
|
||||
<motion.div
|
||||
key={block.label}
|
||||
className="rounded-md border border-surface bg-muted/30 p-1.5 sm:rounded-lg sm:p-2"
|
||||
animate={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: {
|
||||
y: [4, 0, -3, 0, 4],
|
||||
opacity: [0.35, 1, 1, 0.85, 0.35],
|
||||
}
|
||||
}
|
||||
transition={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: {
|
||||
duration: 10,
|
||||
repeat: Infinity,
|
||||
ease: screenLoopEase,
|
||||
delay: 0.35 + i * 0.45,
|
||||
}
|
||||
}
|
||||
>
|
||||
<motion.div
|
||||
className="mb-1.5 size-3 rounded bg-primary/35 sm:size-3.5"
|
||||
animate={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: { scale: [1, 1.15, 1], opacity: [0.55, 1, 0.55] }
|
||||
}
|
||||
transition={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: {
|
||||
duration: 3.6,
|
||||
repeat: Infinity,
|
||||
ease: screenLoopEase,
|
||||
delay: i * 0.5,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<motion.div
|
||||
className={cn(
|
||||
"h-1 origin-right rounded-full bg-foreground/50 sm:h-1.5",
|
||||
block.w
|
||||
)}
|
||||
animate={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: { scaleX: [0.7, 1, 0.85, 1], opacity: [0.5, 1, 0.75, 1] }
|
||||
}
|
||||
transition={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: {
|
||||
duration: 4.2,
|
||||
repeat: Infinity,
|
||||
ease: screenLoopEase,
|
||||
delay: 0.2 + i * 0.35,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div className="mt-1 h-1 w-[70%] rounded-full bg-muted-foreground/25" />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Design cursor — explores nav → CTA click → cards */}
|
||||
{!reduceMotion && (
|
||||
<motion.div
|
||||
className="pointer-events-none absolute z-10 will-change-transform"
|
||||
aria-hidden
|
||||
animate={{
|
||||
opacity: [0, 1, 1, 1, 1, 1, 1, 0],
|
||||
x: ["78%", "18%", "22%", "22%", "55%", "78%", "78%", "78%"],
|
||||
y: ["18%", "14%", "34%", "34%", "78%", "72%", "18%", "18%"],
|
||||
scale: [1, 1, 1, 0.84, 1, 1, 1, 1],
|
||||
}}
|
||||
transition={{
|
||||
duration: 10,
|
||||
repeat: Infinity,
|
||||
ease: screenLoopEase,
|
||||
times: [0, 0.16, 0.32, 0.4, 0.58, 0.74, 0.9, 1],
|
||||
}}
|
||||
>
|
||||
<DesignCursor />
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DesignCursor() {
|
||||
return (
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
className="drop-shadow-sm"
|
||||
>
|
||||
<path
|
||||
d="M5.5 3.5 19 12.2l-6.1 1.4 1.6 6.4-3.1 1.1-1.7-6.6L5.5 3.5Z"
|
||||
className="fill-foreground stroke-background"
|
||||
strokeWidth="1.25"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
+20
-18
@@ -2,12 +2,14 @@ import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Container } from "@/components/container";
|
||||
import Image from "next/image";
|
||||
import { TextEffect } from "@/components/ui/text-effect";
|
||||
import { AnimatedGroup } from "@/components/ui/animated-group";
|
||||
import { HeroHeader } from "./header";
|
||||
import { LogoItem } from "@/components/ui/logo-item";
|
||||
import { getHomeImages } from "@/lib/home-images";
|
||||
import { layout } from "@/lib/layout";
|
||||
import { type, typeSpacing } from "@/lib/typography";
|
||||
|
||||
const transitionVariants = {
|
||||
@@ -40,9 +42,9 @@ export default function HeroSection() {
|
||||
aria-hidden
|
||||
className="absolute inset-0 isolate hidden opacity-65 contain-strict lg:block"
|
||||
>
|
||||
<div className="w-140 h-320 -translate-y-87.5 absolute left-0 top-0 -rotate-45 rounded-full bg-[radial-gradient(68.54%_68.72%_at_55.02%_31.46%,hsla(0,0%,85%,.08)_0,hsla(0,0%,55%,.02)_50%,hsla(0,0%,45%,0)_80%)]" />
|
||||
<div className="h-320 absolute left-0 top-0 w-60 -rotate-45 rounded-full bg-[radial-gradient(50%_50%_at_50%_50%,hsla(0,0%,85%,.06)_0,hsla(0,0%,45%,.02)_80%,transparent_100%)] [translate:5%_-50%]" />
|
||||
<div className="h-320 -translate-y-87.5 absolute left-0 top-0 w-60 -rotate-45 bg-[radial-gradient(50%_50%_at_50%_50%,hsla(0,0%,85%,.04)_0,hsla(0,0%,45%,.02)_80%,transparent_100%)]" />
|
||||
<div className="glow-beam--1 w-140 h-320 -translate-y-87.5 absolute left-0 top-0 -rotate-45 rounded-full" />
|
||||
<div className="glow-beam--2 h-320 absolute left-0 top-0 w-60 -rotate-45 rounded-full [translate:5%_-50%]" />
|
||||
<div className="glow-beam--3 h-320 -translate-y-87.5 absolute left-0 top-0 w-60 -rotate-45" />
|
||||
</div>
|
||||
<section className="bg-background">
|
||||
<div className="relative pt-24 md:pt-36">
|
||||
@@ -85,12 +87,12 @@ export default function HeroSection() {
|
||||
</AnimatedGroup>
|
||||
|
||||
|
||||
<div className="mx-auto max-w-7xl px-6">
|
||||
<Container>
|
||||
<div className="text-center sm:mx-auto lg:mr-auto lg:mt-0">
|
||||
{/* <AnimatedGroup variants={transitionVariants}>
|
||||
<Link
|
||||
href="#link"
|
||||
className="hover:bg-background dark:hover:border-t-border bg-muted group mx-auto flex w-fit items-center gap-4 rounded-full border p-1 ps-4 shadow-md shadow-zinc-950/5 transition-colors duration-300 dark:border-t-white/5 dark:shadow-zinc-950"
|
||||
className="group mx-auto flex w-fit items-center gap-4 rounded-full border bg-muted p-1 ps-4 shadow-md shadow-brand-neutral/5 transition-colors duration-300 hover:bg-background dark:border-t-white/5 dark:shadow-brand-neutral dark:hover:border-t-border"
|
||||
>
|
||||
<span className="text-foreground text-sm">
|
||||
معرفی نسل جدید یکپارچهسازی و ارتباطات هوشمند
|
||||
@@ -114,7 +116,7 @@ export default function HeroSection() {
|
||||
preset="fade-in-blur"
|
||||
speedSegment={0.3}
|
||||
as="h1"
|
||||
className={cn("mx-auto mt-8 max-w-4xl", type.display, "max-md:font-semibold lg:mt-16")}
|
||||
className={cn("mx-auto mt-8 max-w-4xl", type.display, "lg:mt-16")}
|
||||
>
|
||||
راهحلهای مدرن برای اتصال هوشمند
|
||||
|
||||
@@ -149,7 +151,7 @@ export default function HeroSection() {
|
||||
className="bg-foreground/10 rounded-[calc(var(--radius-xl)+0.125rem)] border p-0.5"
|
||||
>
|
||||
<Button asChild size="lg">
|
||||
<Link href="/solutions">
|
||||
<Link href="#">
|
||||
<span className="text-nowrap">مشاهده راهکارها</span>
|
||||
<ChevronLeft className="size-4" aria-hidden />
|
||||
</Link>
|
||||
@@ -157,7 +159,6 @@ export default function HeroSection() {
|
||||
</div>
|
||||
</AnimatedGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatedGroup
|
||||
variants={{
|
||||
@@ -172,14 +173,14 @@ export default function HeroSection() {
|
||||
...transitionVariants,
|
||||
}}
|
||||
>
|
||||
<div className="relative mt-8 overflow-visible px-6 sm:mt-12 md:mt-20 md:p-2 md:px-2">
|
||||
<div className="relative w-full max-w-5xl md:max-w-4xl mx-auto">
|
||||
<div className="relative mt-8 overflow-visible sm:mt-12 md:mt-20">
|
||||
<div className="relative w-full">
|
||||
<div className="relative w-full rounded-2xl">
|
||||
{/* Glow effect */}
|
||||
<div className="absolute inset-0 rounded-2xl bg-gradient-to-r from-blue-500/10 via-purple-500/10 to-pink-500/10 blur-3xl -z-10 scale-110 animate-pulse" />
|
||||
<div className="absolute inset-0 rounded-2xl bg-gradient-to-r from-blue-400/15 via-purple-400/15 to-pink-400/15 blur-2xl -z-10 scale-105" />
|
||||
<div className="absolute inset-0 -z-10 scale-110 rounded-2xl bg-gradient-to-r from-primary/15 via-brand-dark/10 to-primary/10 blur-3xl" />
|
||||
<div className="absolute inset-0 -z-10 scale-105 rounded-2xl bg-gradient-to-r from-primary/20 via-transparent to-brand-dark/15 blur-2xl" />
|
||||
|
||||
<div className="relative w-full aspect-[3/2] overflow-hidden bg-background rounded-2xl border ring-1 ring-border/50 shadow-2xl shadow-blue-500/10 dark:shadow-blue-500/20 md:aspect-auto">
|
||||
<div className="relative aspect-[3/2] w-full overflow-hidden rounded-2xl border bg-background shadow-2xl shadow-primary/10 ring-1 ring-border/50 md:aspect-auto dark:shadow-primary/20">
|
||||
<Image
|
||||
src={hero.main.src}
|
||||
alt="یکپارچهسازی سیستمها و شبکه دیجیتال"
|
||||
@@ -223,10 +224,11 @@ export default function HeroSection() {
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedGroup>
|
||||
</Container>
|
||||
</div>
|
||||
</section>
|
||||
<section className="bg-background pb-16 pt-16 md:pb-32">
|
||||
<div className="m-auto max-w-5xl px-6">
|
||||
<section className={cn("section-shell bg-section-canvas", layout.sectionY)}>
|
||||
<Container>
|
||||
<div className="text-center">
|
||||
<p className={type.eyebrow}>
|
||||
یکپارچه با ابزارهای محبوب
|
||||
@@ -265,14 +267,14 @@ export default function HeroSection() {
|
||||
|
||||
<div className="mt-8 text-center">
|
||||
<Link
|
||||
href="/solutions"
|
||||
className={cn("inline-flex items-center gap-1 transition-colors hover:text-accent", type.link)}
|
||||
href="#"
|
||||
className={cn("inline-flex items-center gap-1 transition-colors hover:text-primary", type.link)}
|
||||
>
|
||||
<span>مشاهده همه یکپارچهسازیها</span>
|
||||
<ChevronLeft className="size-3" aria-hidden />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
|
||||
+11
-11
@@ -23,9 +23,9 @@ export function LoginForm({
|
||||
<form className="p-4 sm:p-6 md:p-8">
|
||||
<FieldGroup className="gap-4 sm:gap-6">
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<h1 className={type.h2}>خوش آمدید</h1>
|
||||
<p className={cn(type["body-sm"], "text-muted-foreground text-balance md:text-base")}>
|
||||
به حساب فیپ خود وارد شوید
|
||||
<h1 className={cn(type.h2, "text-center")}>خوش آمدید</h1>
|
||||
<p className={cn(type["body-sm"], "text-muted-foreground text-balance text-center md:text-base")}>
|
||||
به حساب کاربری خود وارد شوید
|
||||
</p>
|
||||
</div>
|
||||
<Field>
|
||||
@@ -35,7 +35,7 @@ export function LoginForm({
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
placeholder="name@example.com"
|
||||
className="h-10 sm:h-11"
|
||||
required
|
||||
/>
|
||||
@@ -49,7 +49,7 @@ export function LoginForm({
|
||||
رمز عبور
|
||||
</FieldLabel>
|
||||
<Link
|
||||
href="/consulting#consulting-form"
|
||||
href="#"
|
||||
className={cn(type.caption, "sm:ms-auto underline-offset-2 hover:underline")}
|
||||
>
|
||||
رمز عبور را فراموش کردهاید؟
|
||||
@@ -71,11 +71,11 @@ export function LoginForm({
|
||||
یا ادامه با
|
||||
</FieldSeparator>
|
||||
<Field className="">
|
||||
<Button variant="outline" type="button" size="lg" className="w-full">
|
||||
<Button variant="secondary" type="button" size="lg" className="w-full">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
className="w-4 h-4 sm:w-5 sm:h-5"
|
||||
className="size-[var(--btn-icon-size)]"
|
||||
>
|
||||
<path
|
||||
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
||||
@@ -90,15 +90,15 @@ export function LoginForm({
|
||||
<div className="bg-muted relative hidden md:block">
|
||||
<img
|
||||
src="/placeholder.svg"
|
||||
alt="Image"
|
||||
alt="تصویر ورود به حساب کاربری"
|
||||
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<FieldDescription className="px-4 sm:px-6 text-xs sm:text-sm text-center">
|
||||
با کلیک روی ادامه، شما با <Link href="/about" className="underline-offset-2 hover:underline">شرایط استفاده</Link> و{" "}
|
||||
<Link href="/about" className="underline-offset-2 hover:underline">سیاست حریم خصوصی</Link> ما موافقت میکنید.
|
||||
<FieldDescription className={cn(type.caption, "px-4 sm:px-6 text-center")}>
|
||||
با کلیک روی ادامه، شما با <Link href="#" className="underline-offset-2 hover:underline">شرایط استفاده</Link> و{" "}
|
||||
<Link href="#" className="underline-offset-2 hover:underline">سیاست حریم خصوصی</Link> ما موافقت میکنید.
|
||||
</FieldDescription>
|
||||
</div>
|
||||
);
|
||||
|
||||
+43
-121
@@ -1,138 +1,60 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { type } from "@/lib/typography";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Separator } from "./ui/separator";
|
||||
|
||||
import React from 'react';
|
||||
const variants = {
|
||||
mark: {
|
||||
src: "/assets/logo/mainLogo.png",
|
||||
width: 48,
|
||||
height: 48,
|
||||
className: "h-9 w-auto md:h-10",
|
||||
},
|
||||
text: {
|
||||
src: "/assets/logo/mainLogoText.png",
|
||||
width: 180,
|
||||
height: 48,
|
||||
className: "h-9 w-auto md:h-10",
|
||||
},
|
||||
} as const;
|
||||
|
||||
interface LogoProps {
|
||||
color?: string; // رنگ لوگو به صورت پیشفرض سفید است
|
||||
className?: string; // برای اضافه کردن کلاسهای اضافی در صورت نیاز
|
||||
}
|
||||
|
||||
export const LogoType: React.FC<LogoProps> = ({
|
||||
color = '#FFFFFF',
|
||||
className = 'w-12'
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 69.9 34.2"
|
||||
xmlSpace="preserve"
|
||||
className={cn(className, 'fill-primary dark:fill-foreground')}
|
||||
>
|
||||
<path
|
||||
d="M63.5,9.2c-1.2,0-2.5-0.1-3.8-0.1c-2.2,0.3-3.5,2-4,4c0,0.1,0,0.2-0.1,0.3c-0.3,1-0.5,2-0.8,3.1
|
||||
c-6.4,0.3-6.5-0.8-6.1-2.4c0-0.1,0.1-0.2,0.1-0.3c0.4-1.6,0.3-1.3,0.5-2.3c0.1-0.2,0.1-0.5,0.1-0.7c0-0.8-0.1-1.4-0.5-2
|
||||
c-0.4-0.5-0.9-1-1.6-1.3c-1-0.5-1.5-0.3-1.7,0.8c0,0.2-0.1,0.4-0.1,0.7C45.1,10.4,44,15,44,15c-0.4,1.3-0.8,1.5-2.6,1.5
|
||||
c-1.2,0-5.3,0-5.3,0c-1.5-0.2-2.2-0.9-1.9-2.4c0-0.1,0.1-0.2,0.1-0.3c0.4-1.6,0.3-1.3,0.5-2.3c0.1-0.2,0.1-0.5,0.1-0.7
|
||||
c0-0.8-0.1-1.4-0.5-2c-0.4-0.5-0.9-1-1.6-1.3c-1-0.5-1.5-0.3-1.7,0.8c0,0.2-0.1,0.4-0.1,0.7c-0.3,1.5-1.4,6.1-1.4,6.1
|
||||
c-0.4,1.3-0.8,1.5-2.6,1.5c-3.4-0.1-17.9,0.1-21.2,0c-2.5-0.1-3.3,1.4-3.7,2.9l-0.5,1.8c4.6,0.1,27.4,0,27.5,0
|
||||
c0.5,0,0.9-0.1,1.3-0.3c0.7-0.2,1.2-0.6,1.7-1.1c0.6,0.6,1.4,1,2.3,1.2c0.1,0,9.1,0.1,9.2,0.1c1.2-0.1,2.2-0.6,2.9-1.3
|
||||
c0.7,0.7,1.6,1.1,2.7,1.2c0,0,13.7,0.2,13.9,0.2c1.7-0.1,2.5-1.1,3.1-3c0.1-0.5,0.3-0.9,0.4-1.3c0.3-1,0.6-1.9,0.8-2.9
|
||||
c0-0.1,0-0.2,0-0.2C67.9,11.1,66.4,9.3,63.5,9.2z M63,14.2c-0.6,2.4-0.3,2.4-2.6,2.3c-0.2,0-0.2,0-0.5,0c-0.8,0-0.6-0.6-0.6-0.8
|
||||
c0.4-1.9,0.8-2.2,2.7-2.2c0.3,0,1.1,0,1.1,0S63.1,13.9,63,14.2z"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
d="M17.9,24.3c-2.9,0-5,0-7.6,0c-2.6,0-2.9,1.5-3.3,2.6l-0.6,1.7c2.7,0,5.5,0,8.2,0c0.7,0,1.4,0,2.2,0
|
||||
c0.6,0,0.9,0.2,0.7,0.8l-1.1,3.3c0.5,0,1.5,0,2,0c2.2,0,2.6-1,3.2-3.1C23.2,25.3,20.9,24.2,17.9,24.3z"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M64,5.9l-2.7-0.2c-0.5-0.1-0.8-0.6-0.7-1.1l0.6-2.4c0.1-0.5,0.6-0.8,1.1-0.7l2.7,0.2c0.5,0.1,0.8,0.6,0.7,1.1
|
||||
l-0.6,2.4C65,5.7,64.5,6,64,5.9z"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
d="M42.1,24.7c-0.9,0-5.7,0-8.3,0c-1.8,0-1.9,1.3-1.9,1.3l-0.7,3.2l7.8,0c2.3,0,4-1.1,4.3-3.4
|
||||
C43.3,25.1,43,24.7,42.1,24.7z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
export type LogoVariant = keyof typeof variants;
|
||||
|
||||
export const Logo = ({
|
||||
className,
|
||||
uniColor,
|
||||
hasCaption
|
||||
variant = "mark",
|
||||
priority = false,
|
||||
uniColor = false,
|
||||
}: {
|
||||
className?: string;
|
||||
variant?: LogoVariant;
|
||||
priority?: boolean;
|
||||
/** Soft shadow so white wordmark stays readable on light surfaces (keeps brand teal). */
|
||||
uniColor?: boolean;
|
||||
hasCaption?: boolean;
|
||||
}) => {
|
||||
const _feep = <span className={cn(type.h4, "font-bold leading-tight")}><LogoType /></span>
|
||||
const logo = variants[variant];
|
||||
|
||||
return (
|
||||
<Link href="/" className={`flex items-center ${hasCaption ? 'flex-col' : 'gap-2'}`}>
|
||||
<LogoIcon className={cn(className, 'h-7 text-primary')} uniColor={uniColor} />
|
||||
{hasCaption ?
|
||||
<span className="flex flex-col items-center">
|
||||
{_feep}
|
||||
<span className={cn(type.caption, "font-normal leading-tight")}>
|
||||
دادهها فهمیده میشوند
|
||||
</span>
|
||||
<Separator className="mt-2 max-w-10" />
|
||||
</span>
|
||||
: _feep}
|
||||
<Link
|
||||
href="/"
|
||||
aria-label="امین رایان اسپادان"
|
||||
className={cn(
|
||||
"inline-flex items-center transition-opacity hover:opacity-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
src={logo.src}
|
||||
alt="امین رایان اسپادان"
|
||||
width={logo.width}
|
||||
height={logo.height}
|
||||
priority={priority}
|
||||
className={cn(
|
||||
logo.className,
|
||||
uniColor &&
|
||||
"drop-shadow-[0_0_1px_rgba(0,0,0,0.85)] drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)] dark:drop-shadow-none",
|
||||
)}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
interface LogoIconProps {
|
||||
color?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const LogoIcon: React.FC<LogoIconProps> = ({
|
||||
color = '#FFFFFF',
|
||||
className = 'h-5 w-auto md:h-12 dark:invert fill-primary'
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 34.2 34.2"
|
||||
xmlSpace="preserve"
|
||||
className={cn(className, 'fill-primary dark:fill-foreground')}
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
// fill={color}
|
||||
d="M25.2,3.3l-0.5,1.8c-0.4,1.6-1.2,2.8-3.7,2.8c-3.4,0-4.6,0-8,0.1c-2,0-2.3,0.3-2.7,2.2c0,0.2-0.2,0.9,1.2,0.9
|
||||
c4.7,0,9.7,0,14.4,0c3,0,4.6,1.9,4,4.8c-0.3,1.4-0.6,2.7-1.1,4.3c-0.6,2.1-1.4,3.1-3.7,3.1c-2.9,0-5.3,0-8.1,0
|
||||
c-0.9,0-1.4,0.3-1.6,1.3c-0.4,2.2-1,4.4-1.4,6.7c-0.2,0.9-0.6,1-1.3,0.8c-2.2-0.7-2.9-2.6-2.4-4.9c0.5-2.3,1.1-4.6,1.5-6.9
|
||||
c0.2-1.1,0.7-1.6,1.9-1.6c3.3,0.1,6,0.1,9.3,0.1c2.3,0,2,0,2.6-2.4c0.1-0.6-0.1-0.8-0.7-0.8c-0.7,0-1.4,0-2.2,0c-4,0-8,0-12.1,0
|
||||
c-1.1,0-1.5,0.4-1.7,1.4c-0.3,1.7-0.7,3.4-1,5.1c-0.2,1.1-0.7,1.3-1.7,0.8c-1.7-0.8-2.5-2.1-2.1-4C4.9,15,5.7,11.2,6.6,7.4
|
||||
c0.5-2.1,1.9-4,4.3-4.1C11,3.3,20.6,3.3,25.2,3.3z"
|
||||
/>
|
||||
</g>
|
||||
</svg >
|
||||
);
|
||||
};
|
||||
|
||||
export const LogoStroke = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
className={cn("size-7 w-7", className)}
|
||||
viewBox="0 0 71 25"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M61.25 1.625L70.75 1.5625C70.75 4.77083 70.25 7.79167 69.25 10.625C68.2917 13.4583 66.8958 15.9583 65.0625 18.125C63.2708 20.25 61.125 21.9375 58.625 23.1875C56.1667 24.3958 53.4583 25 50.5 25C46.875 25 43.6667 24.2708 40.875 22.8125C38.125 21.3542 35.125 19.2083 31.875 16.375C29.75 14.4167 27.7917 12.8958 26 11.8125C24.2083 10.7292 22.2708 10.1875 20.1875 10.1875C18.0625 10.1875 16.25 10.7083 14.75 11.75C13.25 12.75 12.0833 14.1875 11.25 16.0625C10.4583 17.9375 10.0625 20.1875 10.0625 22.8125L0 22.9375C0 19.6875 0.479167 16.6667 1.4375 13.875C2.4375 11.0833 3.83333 8.64583 5.625 6.5625C7.41667 4.47917 9.54167 2.875 12 1.75C14.5 0.583333 17.2292 0 20.1875 0C23.8542 0 27.1042 0.770833 29.9375 2.3125C32.8125 3.85417 35.7708 5.97917 38.8125 8.6875C41.1042 10.7708 43.1042 12.3333 44.8125 13.375C46.5625 14.375 48.4583 14.875 50.5 14.875C52.6667 14.875 54.5417 14.3125 56.125 13.1875C57.75 12.0625 59 10.5 59.875 8.5C60.7917 6.5 61.25 4.20833 61.25 1.625Z"
|
||||
fill="none"
|
||||
strokeWidth={0.5}
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useRef } from "react";
|
||||
import AutoScroll from "embla-carousel-auto-scroll";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
} from "@/components/ui/carousel";
|
||||
import { Container } from "@/components/container";
|
||||
import { MotionReveal } from "@/components/motion-reveal";
|
||||
import { layout } from "@/lib/layout";
|
||||
import { type } from "@/lib/typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const NORMAL_SPEED = 1;
|
||||
const HOVER_SPEED = 0.35;
|
||||
|
||||
interface Logo {
|
||||
id: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface Logos3Props {
|
||||
heading?: string;
|
||||
logos?: Logo[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultLogos: Logo[] = [
|
||||
{
|
||||
id: "logo-1",
|
||||
description: "Next.js",
|
||||
icon: "logos:nextjs-icon",
|
||||
className: "size-8",
|
||||
},
|
||||
{
|
||||
id: "logo-2",
|
||||
description: "React",
|
||||
icon: "logos:react",
|
||||
className: "size-8",
|
||||
},
|
||||
{
|
||||
id: "logo-3",
|
||||
description: "TypeScript",
|
||||
icon: "logos:typescript-icon",
|
||||
className: "size-8",
|
||||
},
|
||||
{
|
||||
id: "logo-4",
|
||||
description: "Tailwind CSS",
|
||||
icon: "logos:tailwindcss-icon",
|
||||
className: "size-8",
|
||||
},
|
||||
{
|
||||
id: "logo-5",
|
||||
description: "WordPress",
|
||||
icon: "skill-icons:wordpress",
|
||||
className: "size-8",
|
||||
},
|
||||
{
|
||||
id: "logo-6",
|
||||
description: "Google",
|
||||
icon: "logos:google-icon",
|
||||
className: "size-8",
|
||||
},
|
||||
{
|
||||
id: "logo-7",
|
||||
description: "Figma",
|
||||
icon: "logos:figma",
|
||||
className: "size-8",
|
||||
},
|
||||
{
|
||||
id: "logo-8",
|
||||
description: "Vercel",
|
||||
icon: "logos:vercel-icon",
|
||||
className: "size-8",
|
||||
},
|
||||
];
|
||||
|
||||
export function Logos3({
|
||||
heading = "همکاری اخیر ما با برندهای معتبر",
|
||||
logos = defaultLogos,
|
||||
className,
|
||||
}: Logos3Props) {
|
||||
const speedRef = useRef(NORMAL_SPEED);
|
||||
|
||||
// Embla reads `speed` each animation frame via numeric coercion (`valueOf`),
|
||||
// so hover can slow the scroll without stopping or re-initializing the plugin.
|
||||
const autoScrollPlugin = useMemo(
|
||||
() =>
|
||||
AutoScroll({
|
||||
playOnInit: true,
|
||||
speed: {
|
||||
valueOf: () => speedRef.current,
|
||||
} as unknown as number,
|
||||
stopOnInteraction: false,
|
||||
stopOnMouseEnter: false,
|
||||
startDelay: 0,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
"section-shell bg-section-muted",
|
||||
layout.scrollMargin,
|
||||
layout.sectionY,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Container className="section-content flex flex-col items-center text-center">
|
||||
<MotionReveal>
|
||||
<h2 className={cn(type.h2, "text-balance")}>{heading}</h2>
|
||||
</MotionReveal>
|
||||
</Container>
|
||||
<Container className="section-content pt-4 md:pt-14 lg:pt-16">
|
||||
<MotionReveal>
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex items-center justify-center overflow-x-clip",
|
||||
// Fade logos via mask (no painted overlay) so edges match translucent section surfaces
|
||||
"[mask-image:linear-gradient(to_right,transparent,black_12%,black_88%,transparent)]",
|
||||
"[-webkit-mask-image:linear-gradient(to_right,transparent,black_12%,black_88%,transparent)]"
|
||||
)}
|
||||
onMouseEnter={() => {
|
||||
speedRef.current = HOVER_SPEED;
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
speedRef.current = NORMAL_SPEED;
|
||||
}}
|
||||
>
|
||||
<Carousel
|
||||
opts={{ loop: true, direction: "rtl" }}
|
||||
plugins={[autoScrollPlugin]}
|
||||
>
|
||||
<CarouselContent className="ml-0">
|
||||
{logos.map((logo) => (
|
||||
<CarouselItem
|
||||
key={logo.id}
|
||||
className="flex basis-1/3 justify-center pl-0 md:basis-1/5 lg:basis-1/6"
|
||||
>
|
||||
<div className="mx-2 flex shrink-0 items-center justify-center sm:mx-8 md:mx-10">
|
||||
<Icon
|
||||
icon={logo.icon}
|
||||
className={cn(
|
||||
"shrink-0 opacity-70 transition-opacity duration-200 hover:opacity-100",
|
||||
"dark:opacity-80 dark:hover:opacity-100",
|
||||
logo.className
|
||||
)}
|
||||
aria-label={logo.description}
|
||||
/>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
</Carousel>
|
||||
</div>
|
||||
</MotionReveal>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { motion, useReducedMotion } from "motion/react";
|
||||
|
||||
import {
|
||||
blockRevealContainer,
|
||||
blockRevealItem,
|
||||
blockViewport,
|
||||
} from "@/lib/block-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type MotionRevealProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
/** Stagger direct `MotionRevealItem` children (max ~8 for feel). */
|
||||
stagger?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Scroll-driven entrance for page blocks.
|
||||
* Respects `prefers-reduced-motion` and plays once per view.
|
||||
*/
|
||||
export function MotionReveal({
|
||||
children,
|
||||
className,
|
||||
stagger = false,
|
||||
}: MotionRevealProps) {
|
||||
const reduceMotion = useReducedMotion();
|
||||
|
||||
if (reduceMotion) {
|
||||
return <div className={className}>{children}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={cn(className)}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={blockViewport}
|
||||
variants={stagger ? blockRevealContainer : blockRevealItem}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
type MotionRevealItemProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/** Child of a staggered `MotionReveal`. No-op when reduced motion is on. */
|
||||
export function MotionRevealItem({
|
||||
children,
|
||||
className,
|
||||
}: MotionRevealItemProps) {
|
||||
const reduceMotion = useReducedMotion();
|
||||
|
||||
if (reduceMotion) {
|
||||
return <div className={className}>{children}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div className={cn(className)} variants={blockRevealItem}>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ChevronDown,
|
||||
Headphones,
|
||||
LayoutTemplate,
|
||||
Megaphone,
|
||||
Palette,
|
||||
Search,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||
import * as React from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { type } from "@/lib/typography";
|
||||
|
||||
export type MegaMenuItem = {
|
||||
name: string;
|
||||
href: string;
|
||||
description: string;
|
||||
icon: LucideIcon;
|
||||
};
|
||||
|
||||
/** Experimental: service pages not built yet — hrefs stay inert. */
|
||||
export const serviceMegaItems: MegaMenuItem[] = [
|
||||
{
|
||||
name: "طراحی سایت در اصفهان",
|
||||
href: "#",
|
||||
description: "وبسایت حرفهای با تجربه کاربری مدرن",
|
||||
icon: LayoutTemplate,
|
||||
},
|
||||
{
|
||||
name: "سئو در اصفهان",
|
||||
href: "#",
|
||||
description: "رتبه بهتر در گوگل و جذب مشتری هدفمند",
|
||||
icon: Search,
|
||||
},
|
||||
{
|
||||
name: "پشتیبانی سایت",
|
||||
href: "#",
|
||||
description: "نگهداری، امنیت و بهروزرسانی مداوم",
|
||||
icon: Headphones,
|
||||
},
|
||||
{
|
||||
name: "طراحی گرافیک",
|
||||
href: "#",
|
||||
description: "هویت بصری، لوگو و محتوای گرافیکی",
|
||||
icon: Palette,
|
||||
},
|
||||
{
|
||||
name: "ارسال انبوه",
|
||||
href: "#",
|
||||
description: "ارسال انبوه پیامک و کمپینهای دیجیتال",
|
||||
icon: Megaphone,
|
||||
},
|
||||
];
|
||||
|
||||
const CLOSE_DELAY_MS = 140;
|
||||
const PANEL_ANIMATION_MS = 220;
|
||||
|
||||
function isLinkActive(pathname: string, href: string): boolean {
|
||||
if (href === "/") return pathname === "/";
|
||||
if (href.startsWith("/#")) return pathname === "/";
|
||||
return pathname === href || pathname.startsWith(`${href}/`);
|
||||
}
|
||||
|
||||
function MegaMenuCard({
|
||||
item,
|
||||
active,
|
||||
onNavigate,
|
||||
}: {
|
||||
item: MegaMenuItem;
|
||||
active: boolean;
|
||||
onNavigate?: () => void;
|
||||
}) {
|
||||
const Icon = item.icon;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={onNavigate}
|
||||
className={cn(
|
||||
"group/card flex min-h-[5.5rem] gap-3 rounded-2xl border border-transparent p-3",
|
||||
"transition-[transform,background-color,border-color,box-shadow,color] duration-200 ease-out",
|
||||
"hover:-translate-y-[2px] hover:border-border/50 hover:bg-background/45 hover:shadow-surface-sm",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60 focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
||||
active && "border-primary/25 bg-background/40"
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"flex size-11 shrink-0 items-center justify-center rounded-2xl",
|
||||
"border border-border/40 bg-background/40 text-muted-foreground transition-colors duration-200",
|
||||
"group-hover/card:border-primary/30 group-hover/card:bg-primary/10 group-hover/card:text-primary",
|
||||
active && "border-primary/30 bg-primary/10 text-primary"
|
||||
)}
|
||||
>
|
||||
<Icon className="size-5" aria-hidden />
|
||||
</span>
|
||||
<span className="flex min-w-0 flex-1 flex-col justify-center gap-1 text-start">
|
||||
<span className={cn(type.nav, "font-semibold text-foreground")}>
|
||||
{item.name}
|
||||
</span>
|
||||
<span className={cn(type.caption, "line-clamp-2 leading-snug")}>
|
||||
{item.description}
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
type ServicesMegaMenuProps = {
|
||||
label?: string;
|
||||
pathname: string;
|
||||
triggerClassName?: string;
|
||||
active?: boolean;
|
||||
};
|
||||
|
||||
export function ServicesMegaMenu({
|
||||
label = "خدمات",
|
||||
pathname,
|
||||
triggerClassName,
|
||||
active = false,
|
||||
}: ServicesMegaMenuProps) {
|
||||
const reduceMotion = useReducedMotion();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [mounted, setMounted] = React.useState(false);
|
||||
const [panelStyle, setPanelStyle] = React.useState<React.CSSProperties>({});
|
||||
const rootRef = React.useRef<HTMLDivElement>(null);
|
||||
const triggerRef = React.useRef<HTMLButtonElement>(null);
|
||||
const panelRef = React.useRef<HTMLDivElement>(null);
|
||||
const closeTimerRef = React.useRef<number | null>(null);
|
||||
|
||||
const groupActive =
|
||||
active ||
|
||||
serviceMegaItems.some((item) => isLinkActive(pathname, item.href));
|
||||
|
||||
React.useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const clearCloseTimer = React.useCallback(() => {
|
||||
if (closeTimerRef.current !== null) {
|
||||
window.clearTimeout(closeTimerRef.current);
|
||||
closeTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const openMenu = React.useCallback(() => {
|
||||
clearCloseTimer();
|
||||
setOpen(true);
|
||||
}, [clearCloseTimer]);
|
||||
|
||||
const closeMenu = React.useCallback(() => {
|
||||
clearCloseTimer();
|
||||
setOpen(false);
|
||||
}, [clearCloseTimer]);
|
||||
|
||||
const scheduleClose = React.useCallback(() => {
|
||||
clearCloseTimer();
|
||||
closeTimerRef.current = window.setTimeout(() => {
|
||||
setOpen(false);
|
||||
}, CLOSE_DELAY_MS);
|
||||
}, [clearCloseTimer]);
|
||||
|
||||
const updatePanelPosition = React.useCallback(() => {
|
||||
const trigger = triggerRef.current;
|
||||
if (!trigger) return;
|
||||
|
||||
const rect = trigger.getBoundingClientRect();
|
||||
const viewportPadding = 16;
|
||||
const panelWidth = Math.min(352, window.innerWidth - viewportPadding * 2);
|
||||
const isTablet = window.innerWidth < 1280;
|
||||
|
||||
if (isTablet) {
|
||||
setPanelStyle({
|
||||
top: rect.bottom + 8,
|
||||
left: viewportPadding,
|
||||
right: viewportPadding,
|
||||
width: "auto",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const inlineStart = rect.right - panelWidth;
|
||||
const clampedStart = Math.max(
|
||||
viewportPadding,
|
||||
Math.min(inlineStart, window.innerWidth - panelWidth - viewportPadding)
|
||||
);
|
||||
|
||||
setPanelStyle({
|
||||
top: rect.bottom + 8,
|
||||
left: clampedStart,
|
||||
width: panelWidth,
|
||||
});
|
||||
}, []);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
if (!open) return;
|
||||
updatePanelPosition();
|
||||
|
||||
const handleLayout = () => updatePanelPosition();
|
||||
window.addEventListener("resize", handleLayout);
|
||||
window.addEventListener("scroll", handleLayout, { passive: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleLayout);
|
||||
window.removeEventListener("scroll", handleLayout);
|
||||
};
|
||||
}, [open, updatePanelPosition]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
closeMenu();
|
||||
triggerRef.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [closeMenu, open]);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => clearCloseTimer();
|
||||
}, [clearCloseTimer]);
|
||||
|
||||
const handleTriggerKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
openMenu();
|
||||
requestAnimationFrame(() => {
|
||||
panelRef.current
|
||||
?.querySelector<HTMLAnchorElement>("a[href]")
|
||||
?.focus();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const panelMotion = reduceMotion
|
||||
? {
|
||||
initial: { opacity: 0 },
|
||||
animate: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
transition: { duration: 0.15, ease: "easeOut" as const },
|
||||
}
|
||||
: {
|
||||
initial: { opacity: 0, y: -10, scale: 0.98, filter: "blur(10px)" },
|
||||
animate: { opacity: 1, y: 0, scale: 1, filter: "blur(0px)" },
|
||||
exit: { opacity: 0, y: -6, scale: 0.99, filter: "blur(4px)" },
|
||||
transition: { duration: PANEL_ANIMATION_MS / 1000, ease: "easeOut" as const },
|
||||
};
|
||||
|
||||
const overlayMotion = reduceMotion
|
||||
? {
|
||||
initial: { opacity: 0 },
|
||||
animate: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
transition: { duration: 0.15 },
|
||||
}
|
||||
: {
|
||||
initial: { opacity: 0 },
|
||||
animate: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
transition: { duration: PANEL_ANIMATION_MS / 1000, ease: "easeOut" as const },
|
||||
};
|
||||
|
||||
const portal =
|
||||
mounted && typeof document !== "undefined"
|
||||
? createPortal(
|
||||
<AnimatePresence>
|
||||
{open ? (
|
||||
<>
|
||||
<motion.button
|
||||
type="button"
|
||||
aria-hidden
|
||||
tabIndex={-1}
|
||||
className="fixed inset-0 z-40 bg-transparent"
|
||||
{...overlayMotion}
|
||||
onClick={closeMenu}
|
||||
/>
|
||||
<motion.div
|
||||
ref={panelRef}
|
||||
id="services-mega-menu"
|
||||
data-mega-menu-panel
|
||||
role="region"
|
||||
aria-label="منوی خدمات"
|
||||
className={cn(
|
||||
"fixed z-50 overflow-hidden rounded-2xl border border-border/50",
|
||||
"bg-background/75 shadow-sm",
|
||||
"backdrop-blur-xl backdrop-saturate-150",
|
||||
"supports-[backdrop-filter]:bg-background/65",
|
||||
"xl:max-w-none"
|
||||
)}
|
||||
style={panelStyle}
|
||||
{...panelMotion}
|
||||
onPointerEnter={openMenu}
|
||||
onPointerLeave={scheduleClose}
|
||||
>
|
||||
<ul className="flex flex-col gap-1 p-3">
|
||||
{serviceMegaItems.map((item) => (
|
||||
<li key={item.href}>
|
||||
<MegaMenuCard
|
||||
item={item}
|
||||
active={isLinkActive(pathname, item.href)}
|
||||
onNavigate={closeMenu}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
</>
|
||||
) : null}
|
||||
</AnimatePresence>,
|
||||
document.body
|
||||
)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={rootRef}
|
||||
className="relative"
|
||||
onPointerEnter={openMenu}
|
||||
onPointerLeave={scheduleClose}
|
||||
>
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
aria-expanded={open}
|
||||
aria-haspopup="true"
|
||||
aria-controls="services-mega-menu"
|
||||
onClick={() => (open ? closeMenu() : openMenu())}
|
||||
onKeyDown={handleTriggerKeyDown}
|
||||
className={triggerClassName}
|
||||
>
|
||||
<span>{label}</span>
|
||||
<ChevronDown
|
||||
aria-hidden
|
||||
className={cn(
|
||||
"ms-1 size-3.5 opacity-70 transition-transform duration-200",
|
||||
open && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-x-2 -bottom-0.5 h-px origin-center scale-x-0 bg-foreground/40",
|
||||
"transition-transform duration-300 ease-out",
|
||||
groupActive && "scale-x-100 bg-primary/70"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Hover bridge — eliminates gap between trigger and floating panel */}
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
"absolute start-0 end-0 top-full h-3",
|
||||
open ? "pointer-events-auto" : "pointer-events-none"
|
||||
)}
|
||||
onPointerEnter={openMenu}
|
||||
/>
|
||||
|
||||
{portal}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Compact mobile links — title only, no description cards. */
|
||||
export function MegaMenuMobileCards({
|
||||
items,
|
||||
pathname,
|
||||
onNavigate,
|
||||
}: {
|
||||
items: MegaMenuItem[];
|
||||
pathname: string;
|
||||
onNavigate?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<ul className="flex flex-col gap-0.5 ps-1">
|
||||
{items.map((item) => {
|
||||
const active = isLinkActive(pathname, item.href);
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<li key={item.href}>
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={onNavigate}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-2.5 rounded-md px-2.5 py-1.5",
|
||||
"text-muted-foreground/90 transition-[color,background-color] duration-200 ease-out",
|
||||
"hover:bg-muted/60 hover:text-foreground",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60 focus-visible:ring-offset-2",
|
||||
active && "bg-muted/50 text-foreground"
|
||||
)}
|
||||
>
|
||||
<Icon className="size-4 shrink-0 opacity-70" aria-hidden />
|
||||
<span className={cn(type["nav-mobile"], "text-sm leading-tight")}>
|
||||
{item.name}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/** Fixed ambient layers shared by the whole page — sections must not own atmosphere. */
|
||||
export function PageAtmosphere() {
|
||||
return (
|
||||
<div className="page-atmosphere" aria-hidden>
|
||||
<div className="page-atmosphere__gradient" />
|
||||
<div className="page-atmosphere__light page-atmosphere__light--primary" />
|
||||
<div className="page-atmosphere__light page-atmosphere__light--secondary" />
|
||||
<div className="page-atmosphere__light page-atmosphere__light--tertiary" />
|
||||
<div className="page-atmosphere__noise" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Scroll-linked square grid with breathing visibility — lives inside `.page-content`. */
|
||||
export function PageGrid() {
|
||||
return <div className="page-atmosphere__grid" aria-hidden />;
|
||||
}
|
||||
@@ -47,7 +47,7 @@ function AudienceSection({
|
||||
}) {
|
||||
return (
|
||||
<motion.div className="space-y-2" variants={agentFadeUp}>
|
||||
<p className={cn(type.caption, "font-medium uppercase tracking-wide text-muted-foreground")}>
|
||||
<p className={cn(type.caption, "font-medium text-muted-foreground")}>
|
||||
{title}
|
||||
</p>
|
||||
<motion.div
|
||||
@@ -110,12 +110,12 @@ export function AgentPreviewPanel({ className, onClose }: AgentPreviewPanelProps
|
||||
animate="visible"
|
||||
>
|
||||
<motion.div className="space-y-2" variants={agentFadeUp}>
|
||||
<p className={cn(type.caption, "font-medium uppercase tracking-wide text-muted-foreground")}>
|
||||
<p className={cn(type.caption, "font-medium text-muted-foreground")}>
|
||||
سناریو
|
||||
</p>
|
||||
<h3 className={type.h4}>{AGENT_WORKSPACE_TITLE}</h3>
|
||||
<p className={cn(type["body-sm"], "text-muted-foreground")}>
|
||||
سناریوی آزمایشی برای طراحی workflow پشتیبانی و پاسخگویی خودکار به
|
||||
سناریوی آزمایشی برای طراحی گردشکار پشتیبانی و پاسخگویی خودکار به
|
||||
مشتریان.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5 pt-1">
|
||||
@@ -137,7 +137,7 @@ export function AgentPreviewPanel({ className, onClose }: AgentPreviewPanelProps
|
||||
</motion.div>
|
||||
|
||||
<motion.div className="space-y-2" variants={agentFadeUp}>
|
||||
<p className={cn(type.caption, "font-medium uppercase tracking-wide text-muted-foreground")}>
|
||||
<p className={cn(type.caption, "font-medium text-muted-foreground")}>
|
||||
قالب
|
||||
</p>
|
||||
<div className="flex items-center justify-between rounded-xl border bg-muted/30 p-3">
|
||||
@@ -145,14 +145,14 @@ export function AgentPreviewPanel({ className, onClose }: AgentPreviewPanelProps
|
||||
<FileText className="size-4 text-muted-foreground" aria-hidden />
|
||||
<span className={type["body-sm"]}>پاسخگویی خودکار</span>
|
||||
</div>
|
||||
<Button type="button" variant="ghost" size="sm" className="h-7 px-2 text-xs">
|
||||
<Button type="button" variant="ghost" size="sm">
|
||||
تغییر
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className={cn(type.caption, "font-medium uppercase tracking-wide text-muted-foreground")}>
|
||||
<p className={cn(type.caption, "font-medium text-muted-foreground")}>
|
||||
نمای کلی مخاطب
|
||||
</p>
|
||||
<AudienceSection title="نوع شرکت" tags={AGENT_AUDIENCE_TAGS.companyType} />
|
||||
@@ -162,7 +162,7 @@ export function AgentPreviewPanel({ className, onClose }: AgentPreviewPanelProps
|
||||
</div>
|
||||
|
||||
<motion.div className="space-y-2" variants={agentFadeUp}>
|
||||
<p className={cn(type.caption, "font-medium uppercase tracking-wide text-muted-foreground")}>
|
||||
<p className={cn(type.caption, "font-medium text-muted-foreground")}>
|
||||
ساختار سناریو
|
||||
</p>
|
||||
<div className="flex items-center gap-2 rounded-xl border bg-muted/30 px-3 py-2.5">
|
||||
@@ -178,7 +178,7 @@ export function AgentPreviewPanel({ className, onClose }: AgentPreviewPanelProps
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<Button type="button" variant="outline" className="flex-1">
|
||||
<Button type="button" variant="secondary" className="flex-1">
|
||||
زمانبندی
|
||||
</Button>
|
||||
<Button type="button" className="flex-1">
|
||||
|
||||
@@ -44,7 +44,7 @@ export function AgentSphere({ className }: AgentSphereProps) {
|
||||
className="relative size-full rounded-full shadow-lg"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle at 30% 25%, oklch(0.75 0.12 165), #01796F 45%, oklch(0.35 0.08 175) 100%)",
|
||||
"radial-gradient(circle at 30% 25%, color-mix(in srgb, var(--brand-primary) 55%, white), var(--brand-primary) 45%, var(--brand-primary-dark) 100%)",
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-[18%] rounded-full bg-white/20 blur-sm" />
|
||||
|
||||
@@ -75,8 +75,8 @@ function ProgressCard() {
|
||||
</div>
|
||||
<motion.p
|
||||
className={cn(
|
||||
type.caption,
|
||||
"text-center text-[10px] uppercase tracking-wide",
|
||||
type.micro,
|
||||
"text-center",
|
||||
isComplete || isActive
|
||||
? "text-primary"
|
||||
: "text-muted-foreground"
|
||||
|
||||
@@ -142,10 +142,10 @@ export function ChatComposer({
|
||||
type="button"
|
||||
onClick={onSubmit}
|
||||
disabled={!canSubmit}
|
||||
className="h-8 gap-1.5 rounded-full px-4"
|
||||
size="sm"
|
||||
>
|
||||
<Play className="size-3.5 fill-current" aria-hidden />
|
||||
<span className={type.caption}>اجرا</span>
|
||||
<Play className="fill-current" aria-hidden />
|
||||
اجرا
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
@@ -3,10 +3,10 @@ export const AGENT_BREADCRUMB = {
|
||||
current: "دستیار هوشمند",
|
||||
} as const;
|
||||
|
||||
export const AGENT_WORKSPACE_TITLE = "طراحی workflow پشتیبانی — نمونه Q1";
|
||||
export const AGENT_WORKSPACE_TITLE = "طراحی گردشکار پشتیبانی؛ نمونه سهماهه اول";
|
||||
|
||||
export const AGENT_WELCOME = {
|
||||
greeting: "سلام! به دستیار فیپ خوش آمدید",
|
||||
greeting: "سلام! به دستیار امین رایان خوش آمدید",
|
||||
subtitle: "دستیار هوشمند شما برای اتوماسیون و بهینهسازی فرایندها",
|
||||
} as const;
|
||||
|
||||
@@ -14,9 +14,9 @@ export const AGENT_FEATURE_CARDS = [
|
||||
{
|
||||
id: "workflow",
|
||||
icon: "megaphone" as const,
|
||||
title: "طراحی workflow",
|
||||
description: "امتحان کنید: «یک workflow پاسخگویی خودکار طراحی کن»",
|
||||
prompt: "یک workflow پاسخگویی خودکار برای مشتریان طراحی کن.",
|
||||
title: "طراحی گردشکار",
|
||||
description: "امتحان کنید: «یک گردشکار پاسخگویی خودکار طراحی کن»",
|
||||
prompt: "یک گردشکار پاسخگویی خودکار برای مشتریان طراحی کن.",
|
||||
},
|
||||
{
|
||||
id: "report",
|
||||
@@ -69,8 +69,8 @@ export const AGENT_PREVIEW_TAGS = [
|
||||
] as const;
|
||||
|
||||
export const AGENT_AUDIENCE_TAGS = {
|
||||
companyType: ["استارتآپ AI", "SaaS"],
|
||||
stage: ["مرحله A", "رشد"],
|
||||
companyType: ["استارتآپ هوش مصنوعی", "نرمافزار ابری"],
|
||||
stage: ["مرحله الف", "رشد"],
|
||||
companySize: ["۱۰–۵۰ نفر"],
|
||||
region: ["ایران", "خاورمیانه"],
|
||||
} as const;
|
||||
@@ -108,7 +108,7 @@ export const GUIDE_SCENARIOS = [
|
||||
description: "بهبود لحن، وضوح و ساختار متنهای آماده.",
|
||||
},
|
||||
{
|
||||
title: "ایدهپردازی workflow",
|
||||
title: "ایدهپردازی گردشکار",
|
||||
description: "طراحی اولیه جریانهای کاری و اتوماسیون مرتبط.",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import { Container } from "@/components/container";
|
||||
import {
|
||||
GUIDE_BENEFITS,
|
||||
GUIDE_SCENARIOS,
|
||||
GUIDE_USE_CASES,
|
||||
} from "./playground-content";
|
||||
import { layout } from "@/lib/layout";
|
||||
import { type, typeSpacing } from "@/lib/typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function PlaygroundGuide() {
|
||||
return (
|
||||
<div className="border-t bg-muted/20">
|
||||
<section className="py-12 md:py-16">
|
||||
<div className="mx-auto max-w-6xl px-4 sm:px-6">
|
||||
<section className={cn("section-shell", layout.sectionY)}>
|
||||
<Container>
|
||||
<div className={cn("mx-auto max-w-3xl text-center", typeSpacing.sectionStack)}>
|
||||
<h2 className={type.h2}>
|
||||
در آزمایشگاه فیپ چه کارهایی میتوانید انجام دهید؟
|
||||
</h2>
|
||||
<p className={cn(type["body-sm"], "text-muted-foreground md:text-base")}>
|
||||
این بخش برای آشنایی با قابلیتهای آزمایشگاه فیپ است — نه بخشی از
|
||||
این بخش برای آشنایی با قابلیتهای آزمایشگاه فیپ است؛ نه بخشی از
|
||||
تاریخچه گفتگو.
|
||||
</p>
|
||||
</div>
|
||||
@@ -36,11 +38,11 @@ export function PlaygroundGuide() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
<section className="border-t bg-background py-12 md:py-16">
|
||||
<div className="mx-auto max-w-6xl px-4 sm:px-6">
|
||||
<section className={cn("section-shell border-t bg-section-canvas", layout.sectionY)}>
|
||||
<Container>
|
||||
<div className={cn("mx-auto max-w-3xl text-center", typeSpacing.sectionStack)}>
|
||||
<h2 className={type.h2}>
|
||||
سناریوهای قابل آزمایش
|
||||
@@ -63,11 +65,11 @@ export function PlaygroundGuide() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
<section className="border-t bg-muted/30 py-12 md:py-16">
|
||||
<div className="mx-auto max-w-6xl px-4 sm:px-6">
|
||||
<section className={cn("section-shell border-t bg-section-muted", layout.sectionY)}>
|
||||
<Container>
|
||||
<div className="mx-auto max-w-3xl rounded-2xl border bg-card p-6 md:p-8">
|
||||
<h2 className={type.h2}>
|
||||
چرا آزمایشگاه فیپ مهم است؟
|
||||
@@ -88,7 +90,7 @@ export function PlaygroundGuide() {
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,8 +11,8 @@ export const PROMPT_STARTERS = [
|
||||
},
|
||||
{
|
||||
id: "workflow",
|
||||
label: "پیشنهاد workflow",
|
||||
prompt: "برای این فرایند یک workflow پیشنهاد بده:\n\n[فرایند موردنظر]",
|
||||
label: "پیشنهاد گردشکار",
|
||||
prompt: "برای این فرایند یک گردشکار پیشنهاد بده:\n\n[فرایند موردنظر]",
|
||||
},
|
||||
{
|
||||
id: "support-reply",
|
||||
|
||||
@@ -9,8 +9,6 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { type } from "@/lib/typography";
|
||||
import { PLAYGROUND_SCENARIOS } from "./playground-prompts";
|
||||
|
||||
export { PLAYGROUND_SCENARIOS };
|
||||
@@ -48,15 +46,10 @@ export function PlaygroundScenariosCard({
|
||||
<Button
|
||||
key={scenario.id}
|
||||
type="button"
|
||||
variant={activeScenarioId === scenario.id ? "default" : "outline"}
|
||||
variant={activeScenarioId === scenario.id ? "default" : "secondary"}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
onClick={() => onSelectScenario(scenario.id, scenario.prompt)}
|
||||
className={cn(
|
||||
"h-auto min-h-8 whitespace-normal rounded-full px-3 py-1.5",
|
||||
type.caption,
|
||||
activeScenarioId === scenario.id && "shadow-sm"
|
||||
)}
|
||||
>
|
||||
{scenario.label}
|
||||
</Button>
|
||||
|
||||
@@ -99,12 +99,9 @@ export function PlaygroundToolsSheet({
|
||||
<Button
|
||||
key={scenario.id}
|
||||
type="button"
|
||||
variant={activeScenarioId === scenario.id ? "default" : "outline"}
|
||||
className={cn(
|
||||
"h-auto justify-start whitespace-normal px-3 py-2.5 text-start",
|
||||
type["body-sm"],
|
||||
activeScenarioId === scenario.id && "shadow-sm"
|
||||
)}
|
||||
variant={activeScenarioId === scenario.id ? "default" : "secondary"}
|
||||
size="sm"
|
||||
className="h-auto min-h-[var(--btn-height-sm)] justify-start whitespace-normal px-[var(--btn-px-sm)] py-2 text-start"
|
||||
onClick={() => handleScenarioSelect(scenario.id, scenario.prompt)}
|
||||
>
|
||||
{scenario.label}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Container } from "@/components/container";
|
||||
import { Cpu, Sparkles } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { layout } from "@/lib/layout";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const tableData = [
|
||||
{
|
||||
@@ -49,8 +52,8 @@ const tableData = [
|
||||
|
||||
export default function PricingComparator() {
|
||||
return (
|
||||
<section className="bg-background py-16 md:py-32">
|
||||
<div className="mx-auto max-w-5xl px-6">
|
||||
<section className={cn("section-shell bg-section-canvas", layout.sectionY)}>
|
||||
<Container>
|
||||
<div className="w-full overflow-auto lg:overflow-visible">
|
||||
<table className="w-[200vw] border-separate border-spacing-x-3 md:w-full">
|
||||
<thead className="bg-background sticky top-0 z-10">
|
||||
@@ -59,22 +62,22 @@ export default function PricingComparator() {
|
||||
<th className="space-y-3">
|
||||
<span className="block">رایگان</span>
|
||||
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href="/login?utm_source=pricing-free">شروع کنید</Link>
|
||||
<Button asChild variant="secondary" size="sm">
|
||||
<Link href="#">شروع کنید</Link>
|
||||
</Button>
|
||||
</th>
|
||||
<th className="bg-muted rounded-t-xl space-y-3 px-4 border dark:border-transparent border-border">
|
||||
<span className="block">حرفهای</span>
|
||||
<Button asChild size="sm">
|
||||
<Link href="/login?utm_source=pricing-professional">
|
||||
<Link href="#">
|
||||
شروع کنید
|
||||
</Link>
|
||||
</Button>
|
||||
</th>
|
||||
<th className="space-y-3">
|
||||
<span className="block">استارتاپ</span>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href="/login?utm_source=pricing-startup">
|
||||
<Button asChild variant="secondary" size="sm">
|
||||
<Link href="#">
|
||||
شروع کنید
|
||||
</Link>
|
||||
</Button>
|
||||
@@ -231,7 +234,7 @@ export default function PricingComparator() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
+36
-34
@@ -1,6 +1,8 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Container } from "@/components/container";
|
||||
import { Check, FileText, Code, Settings, Users, Clock } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { layout } from "@/lib/layout";
|
||||
import { type, typeSpacing } from "@/lib/typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -8,14 +10,14 @@ export default function Pricing() {
|
||||
return (
|
||||
<>
|
||||
{/* Hero Section */}
|
||||
<section className="bg-background pt-32 pb-20 md:pt-40 md:pb-28">
|
||||
<div className="mx-auto max-w-4xl px-6">
|
||||
<div className={cn("text-center", typeSpacing.heroStack)}>
|
||||
<h1 className={type.h1}>
|
||||
<section className={cn("section-shell bg-section-canvas", layout.sectionTopWithHeader)}>
|
||||
<Container>
|
||||
<div className={cn(typeSpacing.heroStack)}>
|
||||
<h1 className={cn(type.h1, "text-center")}>
|
||||
قیمتگذاری
|
||||
</h1>
|
||||
<div className={cn(typeSpacing.sectionStack, "max-w-3xl mx-auto")}>
|
||||
<p className={cn(type["body-lg"], "text-muted-foreground")}>
|
||||
<div className={cn(typeSpacing.sectionStack, "mx-auto max-w-3xl text-start")}>
|
||||
<p className={type["section-intro"]}>
|
||||
هر راهحل با توجه به پیچیدگی، شرایط اجرا و نیازمندیهای خاص شما طراحی میشود. به همین دلیل، قیمتگذاری ما کاملاً سفارشی و متناسب با شرایط واقعی کسبوکار شماست.
|
||||
</p>
|
||||
<p className={cn(type["section-intro"], "text-foreground font-medium")}>
|
||||
@@ -23,15 +25,15 @@ export default function Pricing() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
{/* Pricing Factors - بخش اصلی توضیحات */}
|
||||
<section className="bg-muted/30 py-20 md:py-28">
|
||||
<div className="mx-auto max-w-6xl px-6">
|
||||
<section className={cn("section-shell bg-section-muted", layout.sectionY)}>
|
||||
<Container>
|
||||
<div className="space-y-12">
|
||||
<div className="text-center space-y-4 max-w-3xl mx-auto">
|
||||
<h2 className={type.h2}>
|
||||
<div className="mx-auto max-w-3xl space-y-4 text-start">
|
||||
<h2 className={cn(type.h2, "text-center")}>
|
||||
راهحلها بر اساس چه عواملی قیمتگذاری میشوند؟
|
||||
</h2>
|
||||
<p className={type["section-intro"]}>
|
||||
@@ -41,7 +43,7 @@ export default function Pricing() {
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="space-y-4 p-8 rounded-2xl border dark:border-transparent bg-card transition-all duration-300 hover:shadow-lg hover:-translate-y-1">
|
||||
<div className="space-y-4 rounded-2xl border border-border bg-card p-8 transition-[border-color,box-shadow,transform] duration-200 hover:-translate-y-1 hover:border-primary/30 hover:shadow-lg dark:hover:shadow-glow-primary">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex size-14 items-center justify-center rounded-xl bg-primary/10 text-primary shrink-0 transition-transform duration-300 group-hover:scale-110">
|
||||
<FileText className="size-7" />
|
||||
@@ -54,7 +56,7 @@ export default function Pricing() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 p-8 rounded-2xl border dark:border-transparent bg-card transition-all duration-300 hover:shadow-lg hover:-translate-y-1">
|
||||
<div className="space-y-4 rounded-2xl border border-border bg-card p-8 transition-[border-color,box-shadow,transform] duration-200 hover:-translate-y-1 hover:border-primary/30 hover:shadow-lg dark:hover:shadow-glow-primary">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex size-14 items-center justify-center rounded-xl bg-primary/10 text-primary shrink-0 transition-transform duration-300 group-hover:scale-110">
|
||||
<Code className="size-7" />
|
||||
@@ -67,7 +69,7 @@ export default function Pricing() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 p-8 rounded-2xl border dark:border-transparent bg-card transition-all duration-300 hover:shadow-lg hover:-translate-y-1">
|
||||
<div className="space-y-4 rounded-2xl border border-border bg-card p-8 transition-[border-color,box-shadow,transform] duration-200 hover:-translate-y-1 hover:border-primary/30 hover:shadow-lg dark:hover:shadow-glow-primary">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex size-14 items-center justify-center rounded-xl bg-primary/10 text-primary shrink-0 transition-transform duration-300 group-hover:scale-110">
|
||||
<Settings className="size-7" />
|
||||
@@ -80,7 +82,7 @@ export default function Pricing() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 p-8 rounded-2xl border dark:border-transparent bg-card transition-all duration-300 hover:shadow-lg hover:-translate-y-1">
|
||||
<div className="space-y-4 rounded-2xl border border-border bg-card p-8 transition-[border-color,box-shadow,transform] duration-200 hover:-translate-y-1 hover:border-primary/30 hover:shadow-lg dark:hover:shadow-glow-primary">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex size-14 items-center justify-center rounded-xl bg-primary/10 text-primary shrink-0 transition-transform duration-300 group-hover:scale-110">
|
||||
<Users className="size-7" />
|
||||
@@ -93,7 +95,7 @@ export default function Pricing() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 p-8 rounded-2xl border dark:border-transparent bg-card transition-all duration-300 hover:shadow-lg hover:-translate-y-1">
|
||||
<div className="space-y-4 rounded-2xl border border-border bg-card p-8 transition-[border-color,box-shadow,transform] duration-200 hover:-translate-y-1 hover:border-primary/30 hover:shadow-lg dark:hover:shadow-glow-primary">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex size-14 items-center justify-center rounded-xl bg-primary/10 text-primary shrink-0 transition-transform duration-300 group-hover:scale-110">
|
||||
<Clock className="size-7" />
|
||||
@@ -106,7 +108,7 @@ export default function Pricing() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 p-8 rounded-2xl border dark:border-transparent bg-card transition-all duration-300 hover:shadow-lg hover:-translate-y-1">
|
||||
<div className="space-y-4 rounded-2xl border border-border bg-card p-8 transition-[border-color,box-shadow,transform] duration-200 hover:-translate-y-1 hover:border-primary/30 hover:shadow-lg dark:hover:shadow-glow-primary">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex size-14 items-center justify-center rounded-xl bg-primary/10 text-primary shrink-0 transition-transform duration-300 group-hover:scale-110">
|
||||
<Settings className="size-7" />
|
||||
@@ -115,21 +117,21 @@ export default function Pricing() {
|
||||
</div>
|
||||
<p className={cn(type.body, "text-muted-foreground")}>
|
||||
نیاز به پشتیبانی فنی، بهروزرسانیها، نگهداری و مستندات کامل
|
||||
از عوامل مهم در تعیین قیمت هستند. پشتیبانی ۲۴/۷ و نگهداری مستمر نیاز به منابع بیشتری دارد.
|
||||
از عوامل مهم در تعیین قیمت هستند. پشتیبانی ۲۴ ساعته و نگهداری مستمر نیاز به منابع بیشتری دارد.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
{/* Package Details */}
|
||||
<section className="bg-background py-20 md:py-28">
|
||||
<div className="mx-auto max-w-6xl px-6">
|
||||
<div className="bg-card relative rounded-3xl border dark:border-transparent shadow-2xl shadow-zinc-950/5">
|
||||
<section className={cn("section-shell bg-section-canvas", layout.sectionY)}>
|
||||
<Container>
|
||||
<div className="relative rounded-3xl border bg-card shadow-2xl shadow-brand-neutral/5 dark:border-border">
|
||||
<div className="grid items-center gap-12 p-12 md:grid-cols-2">
|
||||
<div className="pb-12 text-center md:pb-0 md:pe-12">
|
||||
<h3 className={cn(type.h2, "mb-4")}>راهحل سازمانی</h3>
|
||||
<h3 className={cn(type.h3, "mb-4")}>راهحل سازمانی</h3>
|
||||
<p className={cn(type["section-intro"], "mb-8")}>
|
||||
برای شرکت شما با هر اندازهای
|
||||
</p>
|
||||
@@ -147,10 +149,10 @@ export default function Pricing() {
|
||||
"احراز هویت و مدیریت هویت پیشرفته",
|
||||
"داشبورد تحلیل و گزارشگیری",
|
||||
"ارتباطات چندکاناله (تلگرام، واتساپ و ...)",
|
||||
"اتوماسیون فرآیندها و workflow",
|
||||
"پشتیبانی ۲۴/۷ و مستندات کامل",
|
||||
"اتوماسیون فرآیندها و گردشکار",
|
||||
"پشتیبانی ۲۴ ساعته و مستندات کامل",
|
||||
"ذخیرهسازی نامحدود",
|
||||
"API و Webhooks نامحدود"
|
||||
"رابط برنامهنویسی و وبهوک نامحدود"
|
||||
].map((item, index) => (
|
||||
<li key={index} className={cn("flex items-center gap-3", type.body)}>
|
||||
<Check className="size-5 text-primary shrink-0" />
|
||||
@@ -161,13 +163,13 @@ export default function Pricing() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
{/* CTA Section - اطلاع از قیمت */}
|
||||
<section className="bg-muted/30 py-20 md:py-28">
|
||||
<div className="mx-auto max-w-4xl px-6">
|
||||
<div className="text-center space-y-8 p-12 rounded-2xl border dark:border-transparent bg-gradient-to-br from-primary/5 to-primary/10">
|
||||
<section className={cn("section-shell bg-section-muted", layout.sectionY)}>
|
||||
<Container>
|
||||
<div className="space-y-8 rounded-2xl border border-border bg-gradient-to-br from-primary/8 via-background to-brand-dark/8 p-12 text-center shadow-sm dark:from-primary/15 dark:via-card dark:to-brand-dark/20 dark:shadow-glow-primary">
|
||||
<h2 className={type["cta-title"]}>
|
||||
برای اطلاع از قیمت با ما تماس بگیرید
|
||||
</h2>
|
||||
@@ -177,14 +179,14 @@ export default function Pricing() {
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button asChild size="lg">
|
||||
<Link href="/consulting#consulting-form">شروع گفتگو با تیم</Link>
|
||||
<Link href="#">شروع گفتگو با تیم</Link>
|
||||
</Button>
|
||||
<Button asChild size="lg" variant="outline">
|
||||
<Link href="/about">درباره ما</Link>
|
||||
<Button asChild size="lg" variant="secondary">
|
||||
<Link href="#">درباره ما</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,451 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowLeft, ArrowRight, ArrowUpLeft } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { Container } from "@/components/container";
|
||||
import { MotionReveal } from "@/components/motion-reveal";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { CarouselApi } from "@/components/ui/carousel";
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
} from "@/components/ui/carousel";
|
||||
import { layout } from "@/lib/layout";
|
||||
import { type, typeMeasure, typeSpacing } from "@/lib/typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const AUTOPLAY_MS = 4000;
|
||||
const TWEEN_FACTOR_BASE = 0.84;
|
||||
const SCALE_MIN = 0.8;
|
||||
const SCALE_MAX = 1.05;
|
||||
const OPACITY_MIN = 0.45;
|
||||
const OPACITY_MAX = 1;
|
||||
|
||||
const projects17Prop = [
|
||||
{
|
||||
id: 1,
|
||||
title: "طراحی سایت سنگبری",
|
||||
profession: "سنگبری",
|
||||
image: "/assets/main-pic/100.jpg",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "طراحی سایت فروشگاهی کفش",
|
||||
profession: "فروشگاه کفش",
|
||||
image: "/assets/main-pic/101.jpg",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "طراحی سایت لوازم یدکی خودرو",
|
||||
profession: "لوازم یدکی خودرو",
|
||||
image: "/assets/main-pic/102.jpg",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "طراحی سایت لوازم یدکی",
|
||||
profession: "لوازم یدکی",
|
||||
image: "/assets/main-pic/103.jpg",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "طراحی سایت تجهیزات ساختمانی",
|
||||
profession: "تجهیزات ساختمانی",
|
||||
image: "/assets/main-pic/104.jpg",
|
||||
},
|
||||
];
|
||||
|
||||
function numberWithinRange(value: number, min: number, max: number) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
interface Projects17Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Projects17({ className }: Projects17Props) {
|
||||
const [carouselApi, setCarouselApi] = useState<CarouselApi>();
|
||||
const [canScrollPrev, setCanScrollPrev] = useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = useState(false);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [scrollSnaps, setScrollSnaps] = useState<number[]>([]);
|
||||
const tweenNodesRef = useRef<(HTMLElement | null)[]>([]);
|
||||
const tweenFactorRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!carouselApi) return;
|
||||
|
||||
const updateSelection = () => {
|
||||
setCanScrollPrev(carouselApi.canScrollPrev());
|
||||
setCanScrollNext(carouselApi.canScrollNext());
|
||||
setSelectedIndex(carouselApi.selectedScrollSnap());
|
||||
setScrollSnaps(carouselApi.scrollSnapList());
|
||||
};
|
||||
|
||||
updateSelection();
|
||||
carouselApi.on("select", updateSelection);
|
||||
carouselApi.on("reInit", updateSelection);
|
||||
|
||||
return () => {
|
||||
carouselApi.off("select", updateSelection);
|
||||
carouselApi.off("reInit", updateSelection);
|
||||
};
|
||||
}, [carouselApi]);
|
||||
|
||||
// Continuous scale/opacity while scrolling (desktop only)
|
||||
useEffect(() => {
|
||||
if (!carouselApi) return;
|
||||
|
||||
const desktop = window.matchMedia("(min-width: 768px)");
|
||||
|
||||
const setTweenFactor = () => {
|
||||
tweenFactorRef.current =
|
||||
TWEEN_FACTOR_BASE * carouselApi.scrollSnapList().length;
|
||||
};
|
||||
|
||||
const resetTween = () => {
|
||||
tweenNodesRef.current.forEach((node) => {
|
||||
if (!node) return;
|
||||
node.style.transform = "";
|
||||
node.style.opacity = "";
|
||||
node.style.zIndex = "";
|
||||
});
|
||||
};
|
||||
|
||||
const tweenScale = () => {
|
||||
if (!desktop.matches) {
|
||||
resetTween();
|
||||
return;
|
||||
}
|
||||
|
||||
const engine = carouselApi.internalEngine();
|
||||
const scrollProgress = carouselApi.scrollProgress();
|
||||
const slidesInView = carouselApi.slidesInView();
|
||||
|
||||
carouselApi.scrollSnapList().forEach((scrollSnap, snapIndex) => {
|
||||
let diffToTarget = scrollSnap - scrollProgress;
|
||||
const slidesInSnap = engine.slideRegistry[snapIndex];
|
||||
|
||||
slidesInSnap.forEach((slideIndex) => {
|
||||
const tweenNode = tweenNodesRef.current[slideIndex];
|
||||
if (!tweenNode) return;
|
||||
|
||||
if (engine.options.loop) {
|
||||
engine.slideLooper.loopPoints.forEach((loopItem) => {
|
||||
const target = loopItem.target();
|
||||
if (slideIndex === loopItem.index && target !== 0) {
|
||||
const sign = Math.sign(target);
|
||||
if (sign === -1) {
|
||||
diffToTarget = scrollSnap - (1 + scrollProgress);
|
||||
}
|
||||
if (sign === 1) {
|
||||
diffToTarget = scrollSnap + (1 - scrollProgress);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const tweenValue = 1 - Math.abs(diffToTarget * tweenFactorRef.current);
|
||||
const progress = numberWithinRange(tweenValue, 0, 1);
|
||||
const scale = SCALE_MIN + progress * (SCALE_MAX - SCALE_MIN);
|
||||
const opacity = OPACITY_MIN + progress * (OPACITY_MAX - OPACITY_MIN);
|
||||
|
||||
if (slidesInView.includes(slideIndex) || progress > 0.01) {
|
||||
tweenNode.style.transform = `scale(${scale})`;
|
||||
tweenNode.style.opacity = String(opacity);
|
||||
tweenNode.style.zIndex = progress > 0.85 ? "1" : "0";
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const onReInit = () => {
|
||||
setTweenFactor();
|
||||
tweenScale();
|
||||
};
|
||||
|
||||
const onDesktopChange = () => {
|
||||
setTweenFactor();
|
||||
tweenScale();
|
||||
};
|
||||
|
||||
setTweenFactor();
|
||||
tweenScale();
|
||||
carouselApi.on("reInit", onReInit);
|
||||
carouselApi.on("scroll", tweenScale);
|
||||
carouselApi.on("slideFocus", tweenScale);
|
||||
desktop.addEventListener("change", onDesktopChange);
|
||||
|
||||
return () => {
|
||||
carouselApi.off("reInit", onReInit);
|
||||
carouselApi.off("scroll", tweenScale);
|
||||
carouselApi.off("slideFocus", tweenScale);
|
||||
desktop.removeEventListener("change", onDesktopChange);
|
||||
resetTween();
|
||||
};
|
||||
}, [carouselApi]);
|
||||
|
||||
// Desktop autoplay — paused on hover/focus; skipped for reduced motion & mobile
|
||||
useEffect(() => {
|
||||
if (!carouselApi) return;
|
||||
|
||||
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||
const desktop = window.matchMedia("(min-width: 768px)");
|
||||
let timer: ReturnType<typeof setInterval> | undefined;
|
||||
let paused = false;
|
||||
|
||||
const clear = () => {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const tick = () => {
|
||||
clear();
|
||||
if (paused || reducedMotion.matches || !desktop.matches) return;
|
||||
timer = setInterval(() => {
|
||||
carouselApi.scrollNext();
|
||||
}, AUTOPLAY_MS);
|
||||
};
|
||||
|
||||
const pause = () => {
|
||||
paused = true;
|
||||
clear();
|
||||
};
|
||||
|
||||
const resume = () => {
|
||||
paused = false;
|
||||
tick();
|
||||
};
|
||||
|
||||
const onPointerDown = () => clear();
|
||||
const onSettle = () => {
|
||||
if (!paused) tick();
|
||||
};
|
||||
const onChange = () => tick();
|
||||
|
||||
const root = carouselApi.rootNode();
|
||||
root.addEventListener("mouseenter", pause);
|
||||
root.addEventListener("mouseleave", resume);
|
||||
root.addEventListener("focusin", pause);
|
||||
root.addEventListener("focusout", resume);
|
||||
reducedMotion.addEventListener("change", onChange);
|
||||
desktop.addEventListener("change", onChange);
|
||||
carouselApi.on("pointerDown", onPointerDown);
|
||||
carouselApi.on("settle", onSettle);
|
||||
tick();
|
||||
|
||||
return () => {
|
||||
clear();
|
||||
root.removeEventListener("mouseenter", pause);
|
||||
root.removeEventListener("mouseleave", resume);
|
||||
root.removeEventListener("focusin", pause);
|
||||
root.removeEventListener("focusout", resume);
|
||||
reducedMotion.removeEventListener("change", onChange);
|
||||
desktop.removeEventListener("change", onChange);
|
||||
carouselApi.off("pointerDown", onPointerDown);
|
||||
carouselApi.off("settle", onSettle);
|
||||
};
|
||||
}, [carouselApi]);
|
||||
|
||||
return (
|
||||
<section
|
||||
id="projects"
|
||||
className={cn(
|
||||
"section-shell bg-section-muted",
|
||||
layout.scrollMargin,
|
||||
layout.sectionY,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Container className="section-content">
|
||||
<MotionReveal>
|
||||
<div className="mb-10 flex flex-col gap-6 md:mb-12 md:flex-row md:items-end md:justify-between md:gap-8">
|
||||
<div className={cn(typeSpacing.sectionStack, "min-w-0 max-w-2xl")}>
|
||||
<p className={cn(type.eyebrow, "text-primary")}>پورتفولیو</p>
|
||||
<h2 className={type.h2}>نمونه کارها</h2>
|
||||
<p className={cn(type["section-intro"], typeMeasure.prose)}>
|
||||
نگاهی به وبسایتهایی که برای کسبوکارهای مختلف طراحی و پیادهسازی
|
||||
کردهایم.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden shrink-0 items-center gap-2 md:flex">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="secondary"
|
||||
onClick={() => carouselApi?.scrollPrev()}
|
||||
disabled={!canScrollPrev}
|
||||
aria-label="پروژه قبلی"
|
||||
className="size-10 border-surface bg-background/90 shadow-surface-sm backdrop-blur-sm disabled:opacity-40"
|
||||
>
|
||||
<ArrowRight />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="secondary"
|
||||
onClick={() => carouselApi?.scrollNext()}
|
||||
disabled={!canScrollNext}
|
||||
aria-label="پروژه بعدی"
|
||||
className="size-10 border-surface bg-background/90 shadow-surface-sm backdrop-blur-sm disabled:opacity-40"
|
||||
>
|
||||
<ArrowLeft />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative overflow-hidden">
|
||||
<div className="local-highlight local-highlight--soft" aria-hidden />
|
||||
|
||||
<div className="relative z-[1]">
|
||||
<Carousel
|
||||
setApi={setCarouselApi}
|
||||
opts={{
|
||||
align: "start",
|
||||
loop: true,
|
||||
direction: "rtl",
|
||||
breakpoints: {
|
||||
"(min-width: 768px)": {
|
||||
align: "center",
|
||||
},
|
||||
},
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<CarouselContent
|
||||
className="-mr-3 ml-0 sm:-mr-4 md:-mr-5"
|
||||
viewportClassName="overflow-hidden py-1 sm:py-2 md:py-6"
|
||||
>
|
||||
{projects17Prop.map((project, index) => {
|
||||
const isActive = index === selectedIndex;
|
||||
|
||||
return (
|
||||
<CarouselItem
|
||||
key={project.id}
|
||||
className="basis-[85%] pr-3 pl-0 sm:basis-[70%] sm:pr-4 md:basis-[52%] md:pr-5 lg:basis-[40%]"
|
||||
>
|
||||
<a
|
||||
ref={(node) => {
|
||||
tweenNodesRef.current[index] = node;
|
||||
}}
|
||||
href="#"
|
||||
aria-current={isActive ? "true" : undefined}
|
||||
className={cn(
|
||||
"media-frame group relative block aspect-[4/3] overflow-hidden rounded-xl",
|
||||
"md:origin-center md:will-change-transform",
|
||||
isActive && "md:shadow-glow-media"
|
||||
)}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={project.image}
|
||||
alt={project.title}
|
||||
className={cn(
|
||||
"absolute inset-0 size-full object-cover transition-transform duration-500 ease-out",
|
||||
isActive && "md:group-hover:scale-105"
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 bg-linear-to-t from-brand-neutral/90 via-brand-neutral/35 to-transparent transition-opacity duration-300",
|
||||
isActive && "md:group-hover:opacity-95"
|
||||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="absolute inset-0 flex flex-col justify-end gap-1.5 p-4 sm:gap-2 sm:p-5">
|
||||
<p className={cn(type.caption, "text-white/75")}>
|
||||
{project.profession}
|
||||
</p>
|
||||
<h3
|
||||
className={cn(
|
||||
type.h3,
|
||||
"text-base text-white transition-[font-size] duration-500 ease-out sm:text-lg",
|
||||
isActive && "md:text-xl"
|
||||
)}
|
||||
>
|
||||
{project.title}
|
||||
</h3>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"absolute start-3 top-3 flex size-8 items-center justify-center rounded-full border border-white/20 bg-white/10 text-white backdrop-blur-sm transition-all duration-200 sm:start-4 sm:top-4 sm:size-9",
|
||||
isActive
|
||||
? "opacity-100 md:opacity-0 md:group-hover:opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
aria-hidden
|
||||
>
|
||||
<ArrowUpLeft className="size-4 transition-transform duration-200 group-hover:scale-110 sm:size-5" />
|
||||
</span>
|
||||
</a>
|
||||
</CarouselItem>
|
||||
);
|
||||
})}
|
||||
</CarouselContent>
|
||||
</Carousel>
|
||||
</div>
|
||||
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0 z-10 flex aspect-[4/3] items-center justify-between md:hidden">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="secondary"
|
||||
onClick={() => carouselApi?.scrollPrev()}
|
||||
disabled={!canScrollPrev}
|
||||
aria-label="پروژه قبلی"
|
||||
className="pointer-events-auto size-10 border-surface bg-background/90 shadow-surface-sm backdrop-blur-sm disabled:opacity-40"
|
||||
>
|
||||
<ArrowRight />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="secondary"
|
||||
onClick={() => carouselApi?.scrollNext()}
|
||||
disabled={!canScrollNext}
|
||||
aria-label="پروژه بعدی"
|
||||
className="pointer-events-auto size-10 border-surface bg-background/90 shadow-surface-sm backdrop-blur-sm disabled:opacity-40"
|
||||
>
|
||||
<ArrowLeft />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{scrollSnaps.length > 1 ? (
|
||||
<div
|
||||
className="mt-6 flex items-center justify-center gap-2 md:mt-8"
|
||||
role="tablist"
|
||||
aria-label="اسلایدهای نمونه کار"
|
||||
>
|
||||
{scrollSnaps.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={index === selectedIndex}
|
||||
aria-label={`اسلاید ${index + 1}`}
|
||||
onClick={() => carouselApi?.scrollTo(index)}
|
||||
className={cn(
|
||||
"h-1.5 rounded-full transition-all duration-200",
|
||||
index === selectedIndex
|
||||
? "w-6 bg-primary"
|
||||
: "w-1.5 bg-muted-foreground/30 hover:bg-muted-foreground/50"
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-10 flex justify-center md:mt-12">
|
||||
<Button asChild size="lg">
|
||||
<a href="#">
|
||||
مشاهده نمونه کارها
|
||||
<ArrowLeft />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</MotionReveal>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { RoadmapSection as Roadmap } from "@/components/roadmap/roadmap-section";
|
||||
@@ -0,0 +1,51 @@
|
||||
export type Milestone = {
|
||||
id: number;
|
||||
numeral: string;
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export const milestones: Milestone[] = [
|
||||
{
|
||||
id: 1,
|
||||
numeral: "۱",
|
||||
title: "تماس و بررسی نیازهای کارفرما",
|
||||
description:
|
||||
"نیازهای کارفرما را از طریق وبسایت دریافت و بررسی میکنیم و اهداف پروژه را مشخص میکنیم.",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
numeral: "۲",
|
||||
title: "جلسه با کارفرما",
|
||||
description:
|
||||
"در جلسه اختصاصی، جزئیات پروژه، بودجه و زمانبندی را با کارفرما هماهنگ میکنیم.",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
numeral: "۳",
|
||||
title: "طراحی وایرفریم",
|
||||
description:
|
||||
"ساختار صفحات و تجربه کاربری را پیش از طراحی نهایی بهصورت وایرفریم ارائه میدهیم.",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
numeral: "۴",
|
||||
title: "بررسی وایرفریم با کارفرما",
|
||||
description:
|
||||
"وایرفریم را همراه کارفرما مرور میکنیم و بازخوردها را برای ادامه مسیر اعمال میکنیم.",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
numeral: "۵",
|
||||
title: "لانچ اولیه",
|
||||
description:
|
||||
"نسخه اولیه وبسایت را مستقر میکنیم و عملکرد آن را پیش از تحویل نهایی تست میکنیم.",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
numeral: "۶",
|
||||
title: "تحویل لانچ نهایی به کارفرما",
|
||||
description:
|
||||
"نسخه نهایی پروژه را همراه آموزش و پشتیبانی اولیه به کارفرما تحویل میدهیم.",
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,205 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useLayoutEffect, useRef, useState } from "react";
|
||||
|
||||
import { milestones } from "@/components/roadmap/milestones";
|
||||
import { TimelineItem } from "@/components/roadmap/timeline-item";
|
||||
import { TimelineRail } from "@/components/roadmap/timeline-rail";
|
||||
import {
|
||||
getMilestoneSide,
|
||||
} from "@/components/roadmap/timeline-config";
|
||||
import { useActiveTimelineItem } from "@/components/roadmap/use-active-timeline-item";
|
||||
import { Container } from "@/components/container";
|
||||
import { MotionReveal } from "@/components/motion-reveal";
|
||||
import { layout } from "@/lib/layout";
|
||||
import { type } from "@/lib/typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type RoadmapSectionProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/** Stacked timeline (nodes above cards) below Tailwind `lg`. */
|
||||
const STACKED_RAIL_MQ = "(max-width: 1023px)";
|
||||
|
||||
export function RoadmapSection({ className }: RoadmapSectionProps) {
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
const trackRef = useRef<HTMLDivElement>(null);
|
||||
const nodeRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const cardRefs = useRef<(HTMLElement | null)[]>([]);
|
||||
const [railMetrics, setRailMetrics] = useState({
|
||||
offsetTop: 0,
|
||||
trackHeight: 0,
|
||||
progressHeight: 0,
|
||||
});
|
||||
|
||||
const { activeIndex, setItemRef, selectActiveIndex } = useActiveTimelineItem(
|
||||
milestones.length,
|
||||
sectionRef
|
||||
);
|
||||
|
||||
const setNodeRef = useCallback(
|
||||
(index: number) => (element: HTMLDivElement | null) => {
|
||||
nodeRefs.current[index] = element;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const setCardRef = useCallback(
|
||||
(index: number) => (element: HTMLElement | null) => {
|
||||
cardRefs.current[index] = element;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const updateRailMetrics = useCallback(() => {
|
||||
const track = trackRef.current;
|
||||
const firstNode = nodeRefs.current[0];
|
||||
const lastNode = nodeRefs.current[milestones.length - 1];
|
||||
const activeNode = nodeRefs.current[activeIndex];
|
||||
if (!track || !firstNode || !lastNode || !activeNode) return;
|
||||
|
||||
const trackTop = track.getBoundingClientRect().top;
|
||||
const stacked =
|
||||
typeof window !== "undefined" &&
|
||||
window.matchMedia(STACKED_RAIL_MQ).matches;
|
||||
|
||||
if (stacked) {
|
||||
const firstCard = cardRefs.current[0];
|
||||
const lastCard = cardRefs.current[milestones.length - 1];
|
||||
const activeCard = cardRefs.current[activeIndex];
|
||||
if (!firstCard || !lastCard || !activeCard) return;
|
||||
|
||||
// Card centers keep the rail behind end cards — never above the first
|
||||
// or below the last (nodes sit above cards in the stacked layout).
|
||||
const firstRect = firstCard.getBoundingClientRect();
|
||||
const lastRect = lastCard.getBoundingClientRect();
|
||||
const activeRect = activeCard.getBoundingClientRect();
|
||||
const railStart =
|
||||
firstRect.top + firstRect.height / 2 - trackTop;
|
||||
const railEnd = lastRect.top + lastRect.height / 2 - trackTop;
|
||||
const activeCenter =
|
||||
activeRect.top + activeRect.height / 2 - trackTop;
|
||||
|
||||
setRailMetrics({
|
||||
offsetTop: railStart,
|
||||
trackHeight: Math.max(0, railEnd - railStart),
|
||||
progressHeight: Math.max(0, activeCenter - railStart),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const firstCenter =
|
||||
firstNode.getBoundingClientRect().top + firstNode.offsetHeight / 2;
|
||||
const lastCenter =
|
||||
lastNode.getBoundingClientRect().top + lastNode.offsetHeight / 2;
|
||||
const activeCenter =
|
||||
activeNode.getBoundingClientRect().top + activeNode.offsetHeight / 2;
|
||||
|
||||
setRailMetrics({
|
||||
offsetTop: firstCenter - trackTop,
|
||||
trackHeight: Math.max(0, lastCenter - firstCenter),
|
||||
progressHeight: Math.max(0, activeCenter - firstCenter),
|
||||
});
|
||||
}, [activeIndex]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
updateRailMetrics();
|
||||
window.addEventListener("resize", updateRailMetrics, { passive: true });
|
||||
window.addEventListener("scroll", updateRailMetrics, { passive: true });
|
||||
|
||||
const media = window.matchMedia(STACKED_RAIL_MQ);
|
||||
const onMediaChange = () => updateRailMetrics();
|
||||
media.addEventListener("change", onMediaChange);
|
||||
|
||||
const track = trackRef.current;
|
||||
const observer =
|
||||
track && typeof ResizeObserver !== "undefined"
|
||||
? new ResizeObserver(updateRailMetrics)
|
||||
: null;
|
||||
if (track) observer?.observe(track);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", updateRailMetrics);
|
||||
window.removeEventListener("scroll", updateRailMetrics);
|
||||
media.removeEventListener("change", onMediaChange);
|
||||
observer?.disconnect();
|
||||
};
|
||||
}, [updateRailMetrics]);
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={sectionRef}
|
||||
id="roadmap"
|
||||
className={cn(
|
||||
"timeline-section section-shell bg-section-canvas",
|
||||
layout.sectionY,
|
||||
layout.scrollMargin,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Container className="section-content">
|
||||
<MotionReveal>
|
||||
<header className="relative mb-8 text-center md:mb-10">
|
||||
<div className="local-highlight local-highlight--soft" aria-hidden />
|
||||
<p
|
||||
className={cn(
|
||||
type.eyebrow,
|
||||
"relative mb-2 text-center text-primary"
|
||||
)}
|
||||
>
|
||||
مسیر همکاری
|
||||
</p>
|
||||
<h2
|
||||
className={cn(
|
||||
type.h2,
|
||||
"relative text-center text-balance text-foreground"
|
||||
)}
|
||||
>
|
||||
نقشه راه امین رایان
|
||||
</h2>
|
||||
<p
|
||||
className={cn(
|
||||
type["body-sm"],
|
||||
"relative mx-auto mt-3 max-w-md text-center text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
هر مرحله با اسکرول یا کلیک فعال میشود
|
||||
</p>
|
||||
</header>
|
||||
</MotionReveal>
|
||||
|
||||
{/* dir=ltr keeps physical left/right card alternation consistent */}
|
||||
<div
|
||||
ref={trackRef}
|
||||
dir="ltr"
|
||||
className="timeline-track relative mx-auto max-w-4xl"
|
||||
>
|
||||
<TimelineRail
|
||||
offsetTop={railMetrics.offsetTop}
|
||||
trackHeight={railMetrics.trackHeight}
|
||||
progressHeight={railMetrics.progressHeight}
|
||||
className="left-1/2"
|
||||
/>
|
||||
|
||||
<ol className="relative z-10 m-0 list-none p-0">
|
||||
{milestones.map((milestone, index) => (
|
||||
<TimelineItem
|
||||
key={milestone.id}
|
||||
milestone={milestone}
|
||||
index={index}
|
||||
side={getMilestoneSide(index)}
|
||||
active={activeIndex === index}
|
||||
completed={index < activeIndex}
|
||||
itemRef={setItemRef(index)}
|
||||
nodeRef={setNodeRef(index)}
|
||||
cardRef={setCardRef(index)}
|
||||
onSelect={() => selectActiveIndex(index)}
|
||||
/>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
export const timelineConfig = {
|
||||
/** Vertical gap between steps */
|
||||
stepGap: {
|
||||
mobile: "1.75rem",
|
||||
tablet: "2.25rem",
|
||||
desktop: "2.75rem",
|
||||
},
|
||||
/** Fixed card width on desktop/tablet */
|
||||
cardWidth: {
|
||||
tablet: "18rem",
|
||||
desktop: "20.5rem",
|
||||
},
|
||||
/** Horizontal connector length (rail → card edge) */
|
||||
connectorLength: {
|
||||
mobile: "1.25rem",
|
||||
tablet: "2.5rem",
|
||||
desktop: "3rem",
|
||||
},
|
||||
/** Center rail width */
|
||||
railWidth: 2,
|
||||
/** Node dot size */
|
||||
nodeSize: {
|
||||
mobile: 12,
|
||||
desktop: 14,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export function getMilestoneSide(index: number): "left" | "right" {
|
||||
return index % 2 === 0 ? "left" : "right";
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { motion, useInView, useReducedMotion } from "motion/react";
|
||||
|
||||
import type { Milestone } from "@/components/roadmap/milestones";
|
||||
import { blockEase } from "@/lib/block-motion";
|
||||
import { type } from "@/lib/typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type TimelineItemProps = {
|
||||
milestone: Milestone;
|
||||
index: number;
|
||||
side: "left" | "right";
|
||||
active: boolean;
|
||||
completed: boolean;
|
||||
itemRef: (element: HTMLElement | null) => void;
|
||||
nodeRef: (element: HTMLDivElement | null) => void;
|
||||
/** Mobile stacked card — used to clip the rail to card bounds */
|
||||
cardRef: (element: HTMLElement | null) => void;
|
||||
onSelect: () => void;
|
||||
};
|
||||
|
||||
const cardSpring = {
|
||||
type: "spring" as const,
|
||||
stiffness: 280,
|
||||
damping: 28,
|
||||
mass: 0.85,
|
||||
};
|
||||
|
||||
function TimelineConnector({
|
||||
side,
|
||||
active,
|
||||
completed,
|
||||
}: {
|
||||
side: "left" | "right";
|
||||
active: boolean;
|
||||
completed: boolean;
|
||||
}) {
|
||||
const lit = active || completed;
|
||||
|
||||
return (
|
||||
<svg
|
||||
className={cn(
|
||||
"timeline-connector pointer-events-none absolute top-1/2 z-[5] -translate-y-1/2",
|
||||
side === "left"
|
||||
? "right-1/2 hidden h-4 w-[var(--timeline-connector)] origin-right lg:block"
|
||||
: "left-1/2 hidden h-4 w-[var(--timeline-connector)] origin-left lg:block"
|
||||
)}
|
||||
viewBox="0 0 48 16"
|
||||
preserveAspectRatio="none"
|
||||
aria-hidden
|
||||
>
|
||||
<path
|
||||
d={
|
||||
side === "left"
|
||||
? "M 48 8 H 16 Q 8 8 8 8 L 8 8"
|
||||
: "M 0 8 H 32 Q 40 8 40 8 L 40 8"
|
||||
}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={cn(
|
||||
"text-foreground/12 transition-colors duration-500 ease-out",
|
||||
lit && "text-primary/30"
|
||||
)}
|
||||
/>
|
||||
<path
|
||||
d={
|
||||
side === "left"
|
||||
? "M 48 8 H 18 Q 10 8 10 8"
|
||||
: "M 0 8 H 30 Q 38 8 38 8"
|
||||
}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
className={cn(
|
||||
"transition-all duration-500 ease-out",
|
||||
active
|
||||
? "text-[color:var(--brand-highlight)] opacity-100"
|
||||
: completed
|
||||
? "text-primary/55 opacity-80"
|
||||
: "text-primary/0 opacity-0"
|
||||
)}
|
||||
style={
|
||||
active
|
||||
? {
|
||||
filter:
|
||||
"drop-shadow(0 0 5px color-mix(in srgb, var(--brand-highlight) 75%, transparent))",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function TimelineCard({
|
||||
milestone,
|
||||
side,
|
||||
active,
|
||||
onSelect,
|
||||
}: {
|
||||
milestone: Milestone;
|
||||
side: "left" | "right";
|
||||
active: boolean;
|
||||
onSelect: () => void;
|
||||
}) {
|
||||
const cardRef = useRef<HTMLButtonElement>(null);
|
||||
const reduceMotion = useReducedMotion();
|
||||
const inView = useInView(cardRef, {
|
||||
once: true,
|
||||
amount: 0.35,
|
||||
margin: "0px 0px -8% 0px",
|
||||
});
|
||||
|
||||
const enterX = side === "left" ? -28 : 28;
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
ref={cardRef}
|
||||
type="button"
|
||||
dir="rtl"
|
||||
aria-current={active ? "step" : undefined}
|
||||
aria-label={`${milestone.title}. ${milestone.description}`}
|
||||
onClick={onSelect}
|
||||
initial={false}
|
||||
animate={
|
||||
reduceMotion
|
||||
? { opacity: 1, x: 0, y: 0, scale: 1 }
|
||||
: inView
|
||||
? {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
y: active ? -4 : 0,
|
||||
scale: active ? 1.02 : 1,
|
||||
}
|
||||
: { opacity: 0, x: enterX, y: 18, scale: 0.98 }
|
||||
}
|
||||
whileHover={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: {
|
||||
y: active ? -6 : -3,
|
||||
transition: { duration: 0.2, ease: "easeOut" },
|
||||
}
|
||||
}
|
||||
whileTap={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: { scale: 0.985, transition: { duration: 0.15, ease: "easeOut" } }
|
||||
}
|
||||
transition={
|
||||
reduceMotion
|
||||
? { duration: 0 }
|
||||
: inView
|
||||
? cardSpring
|
||||
: { duration: 0.5, ease: blockEase }
|
||||
}
|
||||
className={cn(
|
||||
"timeline-card relative z-20 block w-full border-0 bg-transparent p-0 text-start focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/70 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent lg:w-[var(--timeline-card-width)]",
|
||||
active ? "timeline-card--active" : "timeline-card--idle"
|
||||
)}
|
||||
>
|
||||
<motion.div
|
||||
className="timeline-card__ambient pointer-events-none absolute start-1/2 top-1/2 -z-10 size-[120%] -translate-x-1/2 -translate-y-1/2 rounded-full bg-glow-primary blur-3xl"
|
||||
aria-hidden
|
||||
initial={false}
|
||||
animate={
|
||||
reduceMotion
|
||||
? { opacity: active ? 0.3 : 0 }
|
||||
: { opacity: active ? 0.3 : 0, scale: active ? 1 : 0.85 }
|
||||
}
|
||||
transition={
|
||||
reduceMotion
|
||||
? { duration: 0 }
|
||||
: { duration: 0.5, ease: blockEase }
|
||||
}
|
||||
/>
|
||||
<div className="timeline-card__surface relative isolate overflow-hidden rounded-2xl border px-4 py-3 backdrop-blur-md sm:px-5 sm:py-3.5">
|
||||
<div
|
||||
className="timeline-card__sheen pointer-events-none absolute inset-0 rounded-2xl"
|
||||
aria-hidden
|
||||
/>
|
||||
|
||||
<div className="relative min-w-0 space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="timeline-card__step grid size-6 shrink-0 place-items-center rounded-full text-[0.8125rem] font-semibold leading-none transition-all duration-500 ease-out">
|
||||
{milestone.numeral}
|
||||
</span>
|
||||
<h3
|
||||
className={cn(
|
||||
type.h4,
|
||||
"timeline-card__title transition-colors duration-500 ease-out"
|
||||
)}
|
||||
>
|
||||
{milestone.title}
|
||||
</h3>
|
||||
</div>
|
||||
<p
|
||||
className={cn(
|
||||
type["body-sm"],
|
||||
"timeline-card__desc leading-relaxed transition-all duration-500 ease-out"
|
||||
)}
|
||||
>
|
||||
{milestone.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.button>
|
||||
);
|
||||
}
|
||||
|
||||
export function TimelineItem({
|
||||
milestone,
|
||||
index,
|
||||
side,
|
||||
active,
|
||||
completed,
|
||||
itemRef,
|
||||
nodeRef,
|
||||
cardRef,
|
||||
onSelect,
|
||||
}: TimelineItemProps) {
|
||||
return (
|
||||
<li
|
||||
ref={itemRef}
|
||||
data-timeline-index={index}
|
||||
className="timeline-item relative"
|
||||
>
|
||||
{/*
|
||||
Mobile/tablet: stacked — centered node, full-width card over the rail.
|
||||
Desktop (lg+): alternating left/right cards with horizontal connectors.
|
||||
*/}
|
||||
<div className="relative flex flex-col lg:grid lg:grid-cols-[1fr_auto_1fr] lg:items-center lg:gap-0">
|
||||
{/* Desktop — left card */}
|
||||
<div
|
||||
className={cn(
|
||||
"relative hidden justify-end pe-[var(--timeline-connector)] lg:flex",
|
||||
side === "left" ? "lg:flex" : "lg:invisible lg:pointer-events-none"
|
||||
)}
|
||||
>
|
||||
{side === "left" ? (
|
||||
<TimelineCard
|
||||
milestone={milestone}
|
||||
side={side}
|
||||
active={active}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Center node — centered above card on mobile/tablet; rail column on desktop */}
|
||||
<div className="relative z-10 order-first mx-auto mb-3 flex w-8 justify-center lg:order-none lg:col-start-2 lg:row-start-1 lg:mx-0 lg:mb-0 lg:w-auto">
|
||||
<div
|
||||
ref={nodeRef}
|
||||
className="timeline-node"
|
||||
data-active={active ? "true" : "false"}
|
||||
data-completed={completed ? "true" : "false"}
|
||||
>
|
||||
<span className="timeline-node__ring" aria-hidden />
|
||||
<span className="timeline-node__core" aria-hidden />
|
||||
</div>
|
||||
<TimelineConnector side={side} active={active} completed={completed} />
|
||||
</div>
|
||||
|
||||
{/* Mobile/tablet card (above rail) + desktop right card.
|
||||
Ref stays on the static wrapper so motion transforms don't skew rail math. */}
|
||||
<div
|
||||
ref={cardRef}
|
||||
className={cn(
|
||||
"relative z-20 min-w-0 lg:col-start-3 lg:row-start-1 lg:flex lg:justify-start lg:ps-[var(--timeline-connector)]",
|
||||
side === "right" ? "lg:block" : "lg:hidden"
|
||||
)}
|
||||
>
|
||||
<TimelineCard
|
||||
milestone={milestone}
|
||||
side={side}
|
||||
active={active}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type TimelineRailProps = {
|
||||
/** Distance from track top to rail start */
|
||||
offsetTop: number;
|
||||
/** Rail length (desktop: first→last node; stacked: first card top→last card bottom) */
|
||||
trackHeight: number;
|
||||
/** Progress fill within the rail (0 → trackHeight) */
|
||||
progressHeight: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function TimelineRail({
|
||||
offsetTop,
|
||||
trackHeight,
|
||||
progressHeight,
|
||||
className,
|
||||
}: TimelineRailProps) {
|
||||
const clampedTrack = Math.max(0, trackHeight);
|
||||
const clampedProgress = Math.min(Math.max(0, progressHeight), clampedTrack);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("timeline-rail pointer-events-none absolute z-0", className)}
|
||||
style={{ top: offsetTop, height: clampedTrack }}
|
||||
aria-hidden
|
||||
>
|
||||
{/* Dim track — ends at the last node, not past the final card */}
|
||||
<div className="timeline-rail__track absolute inset-0 w-[2px] -translate-x-1/2 rounded-full bg-foreground/12" />
|
||||
|
||||
{/* Soft progress accent */}
|
||||
<div
|
||||
className="timeline-rail__progress absolute top-0 w-[2px] -translate-x-1/2 rounded-full bg-gradient-to-b from-primary/35 via-primary to-[color:var(--brand-highlight)] transition-[height,box-shadow] duration-500 ease-out"
|
||||
style={{
|
||||
height: clampedProgress,
|
||||
boxShadow:
|
||||
clampedProgress > 0
|
||||
? "0 0 10px color-mix(in srgb, var(--brand-primary) 40%, transparent), 0 0 22px color-mix(in srgb, var(--brand-highlight) 18%, transparent)"
|
||||
: "none",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState, type RefObject } from "react";
|
||||
|
||||
/** Min time between step changes from wheel / proximity. */
|
||||
const STEP_COOLDOWN_MS = 550;
|
||||
/** Accumulate trackpad deltas before treating as one step intent. */
|
||||
const WHEEL_INTENT_PX = 36;
|
||||
/** Section must cover this band of the viewport to capture wheel. */
|
||||
const LOCK_INSET = 0.18;
|
||||
/** Proximity fallback hysteresis (touch / reduced-motion). */
|
||||
const HYSTERESIS_PX = 48;
|
||||
/** After click, ignore proximity / wheel stepping briefly. */
|
||||
const MANUAL_LOCK_MS = 700;
|
||||
|
||||
function prefersReducedMotion(): boolean {
|
||||
if (typeof window === "undefined") return false;
|
||||
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates timeline steps one-at-a-time.
|
||||
* While the section is in view, wheel/trackpad scroll is captured so each
|
||||
* intentional scroll advances or retreats a single step (no skipping).
|
||||
* Page scroll is released at the first/last step so the user can leave.
|
||||
*/
|
||||
export function useActiveTimelineItem(
|
||||
itemCount: number,
|
||||
sectionRef: RefObject<HTMLElement | null>
|
||||
) {
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const itemRefs = useRef<(HTMLElement | null)[]>([]);
|
||||
const activeIndexRef = useRef(0);
|
||||
const rafRef = useRef<number | null>(null);
|
||||
const cooldownUntilRef = useRef(0);
|
||||
const wheelAccumRef = useRef(0);
|
||||
const manualLockRef = useRef(false);
|
||||
const manualLockTimeoutRef = useRef<number | null>(null);
|
||||
const engagedRef = useRef(false);
|
||||
|
||||
const setItemRef = useCallback(
|
||||
(index: number) => (element: HTMLElement | null) => {
|
||||
itemRefs.current[index] = element;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const scrollStepIntoView = useCallback((index: number) => {
|
||||
const el = itemRefs.current[index];
|
||||
if (!el) return;
|
||||
el.scrollIntoView({
|
||||
behavior: prefersReducedMotion() ? "auto" : "smooth",
|
||||
block: "center",
|
||||
});
|
||||
}, []);
|
||||
|
||||
const goToIndex = useCallback(
|
||||
(index: number, opts?: { scroll?: boolean; cooldown?: boolean }) => {
|
||||
const nextIndex = Math.min(Math.max(index, 0), itemCount - 1);
|
||||
activeIndexRef.current = nextIndex;
|
||||
setActiveIndex(nextIndex);
|
||||
|
||||
if (opts?.scroll !== false) {
|
||||
scrollStepIntoView(nextIndex);
|
||||
}
|
||||
|
||||
if (opts?.cooldown !== false) {
|
||||
cooldownUntilRef.current = performance.now() + STEP_COOLDOWN_MS;
|
||||
}
|
||||
},
|
||||
[itemCount, scrollStepIntoView]
|
||||
);
|
||||
|
||||
// Wheel lock — one scroll gesture → exactly one step (no skip)
|
||||
useEffect(() => {
|
||||
const section = sectionRef.current;
|
||||
if (!section || itemCount < 1) return;
|
||||
if (prefersReducedMotion()) return;
|
||||
|
||||
const isSectionEngaged = () => {
|
||||
const rect = section.getBoundingClientRect();
|
||||
const vh = window.innerHeight;
|
||||
return (
|
||||
rect.top < vh * (1 - LOCK_INSET) && rect.bottom > vh * LOCK_INSET
|
||||
);
|
||||
};
|
||||
|
||||
let lastScrollDir: 1 | -1 = 1;
|
||||
|
||||
const onScroll = () => {
|
||||
const engaged = isSectionEngaged();
|
||||
if (engaged && !engagedRef.current) {
|
||||
// Enter at the edge step for this direction — no snap scroll, no cooldown
|
||||
// so the next wheel gesture immediately advances one step (not two-phase).
|
||||
const edge = lastScrollDir >= 0 ? 0 : itemCount - 1;
|
||||
activeIndexRef.current = edge;
|
||||
setActiveIndex(edge);
|
||||
wheelAccumRef.current = 0;
|
||||
cooldownUntilRef.current = 0;
|
||||
}
|
||||
if (!engaged) {
|
||||
wheelAccumRef.current = 0;
|
||||
}
|
||||
engagedRef.current = engaged;
|
||||
};
|
||||
|
||||
const onWheel = (event: WheelEvent) => {
|
||||
if (manualLockRef.current) return;
|
||||
if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) return;
|
||||
|
||||
if (event.deltaY !== 0) {
|
||||
lastScrollDir = event.deltaY > 0 ? 1 : -1;
|
||||
}
|
||||
|
||||
if (!isSectionEngaged()) {
|
||||
wheelAccumRef.current = 0;
|
||||
engagedRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
engagedRef.current = true;
|
||||
|
||||
const now = performance.now();
|
||||
const current = activeIndexRef.current;
|
||||
const goingDown = event.deltaY > 0;
|
||||
|
||||
// At ends, release so the page can leave the section.
|
||||
if (goingDown && current >= itemCount - 1) {
|
||||
wheelAccumRef.current = 0;
|
||||
return;
|
||||
}
|
||||
if (!goingDown && current <= 0) {
|
||||
wheelAccumRef.current = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture scroll while mid-timeline — prevents skipping steps.
|
||||
event.preventDefault();
|
||||
|
||||
if (now < cooldownUntilRef.current) {
|
||||
wheelAccumRef.current = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
wheelAccumRef.current += event.deltaY;
|
||||
if (Math.abs(wheelAccumRef.current) < WHEEL_INTENT_PX) return;
|
||||
|
||||
const direction = wheelAccumRef.current > 0 ? 1 : -1;
|
||||
wheelAccumRef.current = 0;
|
||||
goToIndex(current + direction);
|
||||
};
|
||||
|
||||
window.addEventListener("wheel", onWheel, { passive: false });
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
return () => {
|
||||
window.removeEventListener("wheel", onWheel);
|
||||
window.removeEventListener("scroll", onScroll);
|
||||
};
|
||||
}, [goToIndex, itemCount, sectionRef]);
|
||||
|
||||
// Proximity fallback for touch, keyboard, and reduced-motion (max ±1 step)
|
||||
useEffect(() => {
|
||||
const section = sectionRef.current;
|
||||
const elements = itemRefs.current.filter(Boolean) as HTMLElement[];
|
||||
if (!section || !elements.length) return;
|
||||
|
||||
const pickActiveIndex = () => {
|
||||
if (manualLockRef.current) return;
|
||||
if (performance.now() < cooldownUntilRef.current) return;
|
||||
|
||||
// Wheel lock owns step changes while the section is engaged.
|
||||
if (!prefersReducedMotion()) {
|
||||
const rect = section.getBoundingClientRect();
|
||||
const vh = window.innerHeight;
|
||||
const engaged =
|
||||
rect.top < vh * (1 - LOCK_INSET) && rect.bottom > vh * LOCK_INSET;
|
||||
if (engaged) return;
|
||||
}
|
||||
|
||||
const viewportCenter = window.innerHeight / 2;
|
||||
const sectionRect = section.getBoundingClientRect();
|
||||
const sectionVisible =
|
||||
sectionRect.top < window.innerHeight * 0.92 &&
|
||||
sectionRect.bottom > window.innerHeight * 0.08;
|
||||
|
||||
if (!sectionVisible) return;
|
||||
|
||||
let closestIndex = 0;
|
||||
let closestDistance = Infinity;
|
||||
|
||||
elements.forEach((element, index) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const elementCenter = rect.top + rect.height / 2;
|
||||
const distance = Math.abs(elementCenter - viewportCenter);
|
||||
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
closestIndex = index;
|
||||
}
|
||||
});
|
||||
|
||||
setActiveIndex((prev) => {
|
||||
if (closestIndex === prev) return prev;
|
||||
|
||||
const prevElement = elements[prev];
|
||||
if (!prevElement) {
|
||||
activeIndexRef.current = closestIndex;
|
||||
return closestIndex;
|
||||
}
|
||||
|
||||
const prevRect = prevElement.getBoundingClientRect();
|
||||
const prevCenter = prevRect.top + prevRect.height / 2;
|
||||
const prevDistance = Math.abs(prevCenter - viewportCenter);
|
||||
|
||||
if (closestDistance > prevDistance - HYSTERESIS_PX) return prev;
|
||||
|
||||
let next = prev;
|
||||
if (closestIndex > prev) next = prev + 1;
|
||||
else if (closestIndex < prev) next = prev - 1;
|
||||
|
||||
activeIndexRef.current = next;
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const scheduleUpdate = () => {
|
||||
if (rafRef.current !== null) return;
|
||||
rafRef.current = window.requestAnimationFrame(() => {
|
||||
rafRef.current = null;
|
||||
pickActiveIndex();
|
||||
});
|
||||
};
|
||||
|
||||
pickActiveIndex();
|
||||
window.addEventListener("scroll", scheduleUpdate, { passive: true });
|
||||
window.addEventListener("resize", scheduleUpdate, { passive: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", scheduleUpdate);
|
||||
window.removeEventListener("resize", scheduleUpdate);
|
||||
if (rafRef.current !== null) {
|
||||
window.cancelAnimationFrame(rafRef.current);
|
||||
}
|
||||
};
|
||||
}, [itemCount, sectionRef]);
|
||||
|
||||
const selectActiveIndex = useCallback(
|
||||
(index: number) => {
|
||||
goToIndex(index, { scroll: true, cooldown: true });
|
||||
|
||||
manualLockRef.current = true;
|
||||
if (manualLockTimeoutRef.current !== null) {
|
||||
window.clearTimeout(manualLockTimeoutRef.current);
|
||||
}
|
||||
manualLockTimeoutRef.current = window.setTimeout(() => {
|
||||
manualLockRef.current = false;
|
||||
manualLockTimeoutRef.current = null;
|
||||
}, MANUAL_LOCK_MS);
|
||||
},
|
||||
[goToIndex]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (manualLockTimeoutRef.current !== null) {
|
||||
window.clearTimeout(manualLockTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { activeIndex, setItemRef, selectActiveIndex };
|
||||
}
|
||||
@@ -42,12 +42,11 @@ export function ScrollToTop({ className }: { className?: string }) {
|
||||
>
|
||||
<Button
|
||||
size="icon-lg"
|
||||
variant="outline"
|
||||
variant="secondary"
|
||||
onClick={scrollToTop}
|
||||
aria-label="برگشت به بالا"
|
||||
className="shadow-md"
|
||||
>
|
||||
<ChevronUp className="size-5" />
|
||||
<ChevronUp />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type AtmosphereVariant = "featured" | "accent";
|
||||
|
||||
interface SectionAtmosphereProps {
|
||||
variant?: AtmosphereVariant;
|
||||
/** @deprecated Grid is owned by PageAtmosphere */
|
||||
pattern?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Prefer `PageAtmosphere` on the page shell.
|
||||
* Sections must not own ambient lighting, grids, or gradient washes.
|
||||
* This component renders nothing and exists only for transitional imports.
|
||||
*/
|
||||
export function SectionAtmosphere({ className }: SectionAtmosphereProps) {
|
||||
return <div className={cn("hidden", className)} aria-hidden />;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "motion/react";
|
||||
import { ArrowUpLeft } from "lucide-react";
|
||||
|
||||
import { Container } from "@/components/container";
|
||||
import { MotionReveal, MotionRevealItem } from "@/components/motion-reveal";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { layout } from "@/lib/layout";
|
||||
import { type } from "@/lib/typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ServiceProps = {
|
||||
title: string;
|
||||
image: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
const services: ServiceProps[] = [
|
||||
{
|
||||
title: "طراحی سایت",
|
||||
image: "/assets/main-pic/11.jpg",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "سئو سایت",
|
||||
image: "/assets/main-pic/22.jpg",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "پشتیبانی سایت",
|
||||
image: "/assets/main-pic/33.jpg",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "ارسال انبوه",
|
||||
image: "/assets/main-pic/44.jpg",
|
||||
url: "#",
|
||||
},
|
||||
];
|
||||
|
||||
interface Services14Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Services14({ className }: Services14Props) {
|
||||
return (
|
||||
<section
|
||||
id="services"
|
||||
className={cn(
|
||||
"section-shell bg-section-canvas",
|
||||
layout.scrollMargin,
|
||||
layout.sectionY,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Container className="section-content grid grid-cols-1 gap-10 md:gap-12 lg:grid-cols-2">
|
||||
<MotionReveal className="flex flex-col">
|
||||
<div>
|
||||
<h2 className={cn(type.h2, "mb-4 text-foreground")}>خدمات ما</h2>
|
||||
<p className={cn(type["section-intro"], "max-w-md lg:max-w-80")}>
|
||||
از طراحی و توسعه وبسایت تا سئو و پشتیبانی؛ راهحلهای دیجیتال
|
||||
سرتاسری برای رشد کسبوکار شما.
|
||||
</p>
|
||||
</div>
|
||||
<Button size="lg" className="mt-6 w-full sm:w-fit" asChild>
|
||||
<a href="#contact">
|
||||
درخواست مشاوره
|
||||
<ArrowUpLeft />
|
||||
</a>
|
||||
</Button>
|
||||
</MotionReveal>
|
||||
|
||||
<div className="relative">
|
||||
<div className="local-highlight local-highlight--soft" aria-hidden />
|
||||
<MotionReveal
|
||||
stagger
|
||||
className="relative z-[1] grid grid-cols-2 gap-3 sm:gap-4"
|
||||
>
|
||||
{services.map((service) => (
|
||||
<MotionRevealItem key={service.title}>
|
||||
<motion.a
|
||||
href={service.url}
|
||||
whileHover={{ opacity: 0.85 }}
|
||||
className="media-frame group block overflow-hidden rounded-xl transition-opacity duration-200"
|
||||
>
|
||||
<Card className="relative aspect-square overflow-hidden border-0 bg-transparent p-0 shadow-none hover:shadow-none">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={service.image}
|
||||
alt={service.title}
|
||||
className="absolute inset-0 size-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-linear-to-t from-brand-neutral/80 via-brand-neutral/25 to-transparent" />
|
||||
<CardContent className="absolute inset-0 flex flex-col justify-end p-3 sm:p-4">
|
||||
<div
|
||||
className={cn(
|
||||
type["card-title"],
|
||||
"text-sm text-white sm:text-base"
|
||||
)}
|
||||
>
|
||||
{service.title}
|
||||
</div>
|
||||
</CardContent>
|
||||
<ArrowUpLeft className="absolute start-3 top-3 size-4 text-white transition-transform group-hover:scale-110 sm:start-4 sm:top-4 sm:size-5" />
|
||||
</Card>
|
||||
</motion.a>
|
||||
</MotionRevealItem>
|
||||
))}
|
||||
</MotionReveal>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
+24
-21
@@ -1,42 +1,45 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import { Moon, Sun } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, setTheme } = useTheme()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
export function ThemeToggle({ className }: { className?: string }) {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-lg"
|
||||
className="rounded-lg"
|
||||
size="icon"
|
||||
className={cn("relative", className)}
|
||||
aria-label="تغییر تم"
|
||||
>
|
||||
<Sun className="size-5" />
|
||||
<Sun />
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-lg"
|
||||
className="rounded-lg"
|
||||
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
||||
size="icon"
|
||||
className={cn(
|
||||
"relative text-muted-foreground hover:text-foreground",
|
||||
className
|
||||
)}
|
||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||
aria-label="تغییر تم"
|
||||
>
|
||||
<Sun className="size-5 rotate-0 scale-100 transition-all duration-300 dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute size-5 rotate-90 scale-0 transition-all duration-300 dark:rotate-0 dark:scale-100" />
|
||||
<Sun className="rotate-0 scale-100 transition-all duration-300 dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute rotate-90 scale-0 transition-all duration-300 dark:rotate-0 dark:scale-100" />
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ function AccordionTrigger({
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-start text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -70,7 +70,7 @@ function AlertDialogHeader({
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-start", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -136,7 +136,7 @@ function AlertDialogCancel({
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||
className={cn(buttonVariants({ variant: "secondary" }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { type } from "@/lib/typography"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
@@ -39,7 +40,8 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
type["list-title"],
|
||||
"col-start-2 line-clamp-1 min-h-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -10,13 +10,13 @@ const badgeVariants = cva(
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary-hover",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
"border-transparent bg-destructive text-destructive-foreground [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
"border-surface text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
"use client"
|
||||
|
||||
import { motion, MotionStyle, Transition } from "motion/react"
|
||||
import { motion, useReducedMotion, type Transition } from "motion/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface BorderBeamProps {
|
||||
/**
|
||||
* The size of the border beam.
|
||||
* Approximate length of the visible beam. Mapped to conic spread.
|
||||
*/
|
||||
size?: number
|
||||
/**
|
||||
* The duration of the border beam.
|
||||
* The duration of one full loop (seconds).
|
||||
*/
|
||||
duration?: number
|
||||
/**
|
||||
@@ -51,49 +51,58 @@ interface BorderBeamProps {
|
||||
borderWidth?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft beam riding the parent border via a masked rotating conic-gradient.
|
||||
* Avoids offset-path corner breaks while staying flush with rounded frames.
|
||||
*/
|
||||
export const BorderBeam = ({
|
||||
className,
|
||||
size = 50,
|
||||
delay = 0,
|
||||
duration = 6,
|
||||
colorFrom = "#FCC61D",
|
||||
colorTo = "#01796F",
|
||||
colorFrom = "#29A19C",
|
||||
colorTo = "#044343",
|
||||
transition,
|
||||
style,
|
||||
reverse = false,
|
||||
initialOffset = 0,
|
||||
borderWidth = 1,
|
||||
}: BorderBeamProps) => {
|
||||
const reduceMotion = useReducedMotion()
|
||||
// Map legacy px "size" → angular span (deg). Clamped so the beam stays a short arc.
|
||||
const spread = Math.min(48, Math.max(12, size * 0.18))
|
||||
const startAngle = initialOffset * 3.6
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 rounded-[inherit] border-(length:--border-beam-width) border-transparent [mask-image:linear-gradient(transparent,transparent),linear-gradient(#000,#000)] [mask-composite:intersect] [mask-clip:padding-box,border-box]"
|
||||
className="pointer-events-none absolute overflow-hidden rounded-[inherit] border-(length:--border-beam-width) border-transparent [mask-image:linear-gradient(transparent,transparent),linear-gradient(#000,#000)] [mask-composite:intersect] [mask-clip:padding-box,border-box]"
|
||||
style={
|
||||
{
|
||||
// Overlay the parent’s border (e.g. .media-frame 1px).
|
||||
inset: `-${borderWidth}px`,
|
||||
"--border-beam-width": `${borderWidth}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<motion.div
|
||||
className={cn(
|
||||
"absolute aspect-square",
|
||||
"bg-gradient-to-l from-[var(--color-from)] via-[var(--color-to)] to-transparent",
|
||||
"absolute top-1/2 left-1/2 aspect-square w-[max(200%,200vw)]",
|
||||
className
|
||||
)}
|
||||
style={
|
||||
{
|
||||
width: size,
|
||||
offsetPath: `rect(0 auto auto 0 round ${size}px)`,
|
||||
"--color-from": colorFrom,
|
||||
"--color-to": colorTo,
|
||||
x: "-50%",
|
||||
y: "-50%",
|
||||
background: `conic-gradient(from ${startAngle}deg, transparent 0deg, ${colorFrom} ${spread * 0.35}deg, ${colorTo} ${spread}deg, transparent ${spread * 1.65}deg)`,
|
||||
...style,
|
||||
} as MotionStyle
|
||||
} as React.CSSProperties
|
||||
}
|
||||
initial={false}
|
||||
animate={
|
||||
reduceMotion
|
||||
? { rotate: 0 }
|
||||
: { rotate: reverse ? -360 : 360 }
|
||||
}
|
||||
initial={{ offsetDistance: `${initialOffset}%` }}
|
||||
animate={{
|
||||
offsetDistance: reverse
|
||||
? [`${100 - initialOffset}%`, `${-initialOffset}%`]
|
||||
: [`${initialOffset}%`, `${100 + initialOffset}%`],
|
||||
}}
|
||||
transition={{
|
||||
repeat: Infinity,
|
||||
ease: "linear",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
import { ChevronLeft, MoreHorizontal } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
||||
return <nav aria-label="مسیر صفحه" data-slot="breadcrumb" {...props} />
|
||||
}
|
||||
|
||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||
@@ -75,7 +75,7 @@ function BreadcrumbSeparator({
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
{children ?? <ChevronLeft />}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
@@ -93,7 +93,7 @@ function BreadcrumbEllipsis({
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
<span className="sr-only">More</span>
|
||||
<span className="sr-only">بیشتر</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
+102
-16
@@ -2,37 +2,119 @@ import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { type as typeScale } from "@/lib/typography"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
/**
|
||||
* Unified button system
|
||||
*
|
||||
* Variants (marketing hierarchy):
|
||||
* - default → Primary (main CTA / conversion)
|
||||
* - secondary → Secondary (bordered supporting action)
|
||||
* - ghost → Ghost (minimal / nav)
|
||||
*
|
||||
* Aliases kept for shadcn / existing blocks:
|
||||
* - outline → same as secondary
|
||||
*
|
||||
* Utility variants:
|
||||
* - destructive, link
|
||||
*
|
||||
* Surfaces:
|
||||
* - default → normal page canvas
|
||||
* - brand → teal / dark marketing panels (inverse contrast)
|
||||
*
|
||||
* Sizes: sm | default (medium) | lg | icon-sm | icon | icon-lg
|
||||
*
|
||||
* Persian text: padding and height are sized for IRANSansX so glyphs
|
||||
* are not clipped; labels use `type.button` / `type["button-lg"]`.
|
||||
*/
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
[
|
||||
"inline-flex items-center justify-center whitespace-nowrap",
|
||||
"rounded-[var(--btn-radius)]",
|
||||
"transition-[color,background-color,border-color,box-shadow,opacity,transform]",
|
||||
"duration-200 ease-out",
|
||||
"outline-none select-none shrink-0",
|
||||
"disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none",
|
||||
"[&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
"[&_svg:not([class*='size-'])]:size-[var(--btn-icon-size)]",
|
||||
"focus-visible:ring-[3px] focus-visible:ring-ring/40 focus-visible:border-ring",
|
||||
"active:scale-[0.98]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
].join(" "),
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
// Primary — brand neon rim via shadow-glow-*
|
||||
default:
|
||||
"bg-primary text-primary-foreground hover:bg-accent hover:text-foreground dark:hover:bg-[#FCC61D] dark:hover:text-foreground",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border border-primary text-primary bg-transparent hover:bg-accent hover:text-foreground hover:border-accent dark:bg-input/30 dark:border-input dark:text-foreground dark:hover:bg-[#FCC61D] dark:hover:text-foreground",
|
||||
"bg-primary text-primary-foreground shadow-glow-sm hover:bg-primary-hover hover:shadow-glow-primary active:bg-primary-active",
|
||||
// Secondary (bordered) — preferred name
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-accent hover:text-foreground dark:hover:bg-[#FCC61D] dark:hover:text-foreground",
|
||||
"border border-surface bg-transparent text-foreground shadow-surface-sm hover:border-primary/45 hover:bg-accent hover:text-accent-foreground hover:shadow-glow-sm active:bg-accent/80 dark:border-white/18 dark:hover:border-primary/50 dark:hover:bg-accent",
|
||||
// Alias of secondary (shadcn / imported blocks)
|
||||
outline:
|
||||
"border border-surface bg-transparent text-foreground shadow-surface-sm hover:border-primary/45 hover:bg-accent hover:text-accent-foreground hover:shadow-glow-sm active:bg-accent/80 dark:border-white/18 dark:hover:border-primary/50 dark:hover:bg-accent",
|
||||
// Ghost
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-foreground dark:hover:bg-[#FCC61D] dark:hover:text-foreground",
|
||||
"bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground active:bg-accent/80 dark:hover:bg-white/8",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90 hover:shadow-md focus-visible:ring-destructive/25 dark:focus-visible:ring-destructive/40 active:bg-destructive/85",
|
||||
link:
|
||||
"text-accent underline-offset-4 hover:underline",
|
||||
"bg-transparent text-primary underline-offset-4 hover:underline hover:text-primary-hover active:text-primary-active",
|
||||
},
|
||||
surface: {
|
||||
default: "",
|
||||
/** Inverse contrast for brand / dark marketing panels */
|
||||
brand: "",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 text-sm has-[>svg]:px-3",
|
||||
sm: "h-8 gap-1.5 px-3 text-sm has-[>svg]:px-2.5",
|
||||
lg: "h-11 px-8 text-base has-[>svg]:px-6",
|
||||
icon: "size-9",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
sm: cn(
|
||||
"h-[var(--btn-height-sm)] gap-[var(--btn-gap-sm)] px-[var(--btn-px-sm)] [--btn-icon-size:0.875rem] has-[>svg]:px-[calc(var(--btn-px-sm)-0.125rem)]",
|
||||
typeScale.button
|
||||
),
|
||||
default: cn(
|
||||
"h-[var(--btn-height-md)] gap-[var(--btn-gap-md)] px-[var(--btn-px-md)] [--btn-icon-size:1rem] has-[>svg]:px-[calc(var(--btn-px-md)-0.25rem)]",
|
||||
typeScale.button
|
||||
),
|
||||
lg: cn(
|
||||
"h-[var(--btn-height-lg)] gap-[var(--btn-gap-lg)] px-[var(--btn-px-lg)] [--btn-icon-size:1.25rem] has-[>svg]:px-[calc(var(--btn-px-lg)-0.25rem)]",
|
||||
typeScale["button-lg"]
|
||||
),
|
||||
"icon-sm":
|
||||
"size-[var(--btn-height-sm)] gap-0 p-0 [--btn-icon-size:0.875rem]",
|
||||
icon: "size-[var(--btn-height-md)] gap-0 p-0 [--btn-icon-size:1rem]",
|
||||
"icon-lg":
|
||||
"size-[var(--btn-height-lg)] gap-0 p-0 [--btn-icon-size:1.25rem]",
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
variant: "default",
|
||||
surface: "brand",
|
||||
className:
|
||||
"bg-white text-brand-dark shadow-glow-sm hover:bg-brand-light hover:text-brand-dark hover:shadow-glow-primary active:bg-brand-light/90",
|
||||
},
|
||||
{
|
||||
variant: ["secondary", "outline"],
|
||||
surface: "brand",
|
||||
className:
|
||||
"border-white/40 bg-transparent text-white shadow-none hover:border-white/60 hover:bg-white/10 hover:text-white hover:shadow-[0_0_0_1px_rgb(255_255_255_/_0.35),0_0_18px_-4px_rgb(255_255_255_/_0.35)] active:bg-white/15",
|
||||
},
|
||||
{
|
||||
variant: "ghost",
|
||||
surface: "brand",
|
||||
className:
|
||||
"text-white hover:bg-white/10 hover:text-white active:bg-white/15",
|
||||
},
|
||||
{
|
||||
variant: "link",
|
||||
surface: "brand",
|
||||
className: "text-white hover:text-white/90",
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
surface: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
@@ -42,6 +124,7 @@ function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
surface,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
@@ -53,7 +136,10 @@ function Button({
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size }), className)}
|
||||
data-variant={variant ?? "default"}
|
||||
data-surface={surface ?? "default"}
|
||||
data-size={size ?? "default"}
|
||||
className={cn(buttonVariants({ variant, size, surface }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm transition-all duration-300 hover:shadow-md hover:border-border/80",
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border border-surface py-6 shadow-glow-sm transition-[border-color,box-shadow] duration-200 hover:border-primary/30 hover:shadow-glow-primary dark:hover:border-primary/40 dark:hover:shadow-glow-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -132,13 +132,20 @@ function Carousel({
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CarouselContent({
|
||||
className,
|
||||
viewportClassName,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
/** Classes for the Embla viewport (`overflow-hidden`). Use for glow/shadow bleed padding. */
|
||||
viewportClassName?: string
|
||||
}) {
|
||||
const { carouselRef, orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={carouselRef}
|
||||
className="overflow-hidden"
|
||||
className={cn("overflow-hidden", viewportClassName)}
|
||||
data-slot="carousel-content"
|
||||
>
|
||||
<div
|
||||
@@ -173,7 +180,7 @@ function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
||||
function CarouselPrevious({
|
||||
className,
|
||||
variant = "outline",
|
||||
variant = "secondary",
|
||||
size = "icon",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
@@ -185,7 +192,7 @@ function CarouselPrevious({
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute size-8 rounded-full",
|
||||
"absolute",
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -left-12 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
@@ -196,14 +203,14 @@ function CarouselPrevious({
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeft />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
<span className="sr-only">اسلاید قبلی</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselNext({
|
||||
className,
|
||||
variant = "outline",
|
||||
variant = "secondary",
|
||||
size = "icon",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
@@ -215,7 +222,7 @@ function CarouselNext({
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute size-8 rounded-full",
|
||||
"absolute",
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -right-12 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
@@ -226,7 +233,7 @@ function CarouselNext({
|
||||
{...props}
|
||||
>
|
||||
<ArrowRight />
|
||||
<span className="sr-only">Next slide</span>
|
||||
<span className="sr-only">اسلاید بعدی</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-start", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -77,7 +77,7 @@ function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
<div
|
||||
data-slot="drawer-header"
|
||||
className={cn(
|
||||
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
|
||||
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-start",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { type } from "@/lib/typography"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Empty({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@@ -62,7 +63,7 @@ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-title"
|
||||
className={cn("text-lg font-medium tracking-tight", className)}
|
||||
className={cn(type.h3, "text-center font-medium", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -73,7 +74,7 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
<div
|
||||
data-slot="empty-description"
|
||||
className={cn(
|
||||
"text-muted-foreground [&>a]:text-accent text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4 hover:[&>a]:no-underline",
|
||||
"text-muted-foreground [&>a]:text-primary text-sm leading-relaxed [&>a]:underline [&>a]:underline-offset-4 hover:[&>a]:no-underline",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -145,7 +145,7 @@ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
className={cn(
|
||||
"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
|
||||
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
|
||||
"[&>a]:text-accent [&>a]:underline [&>a]:underline-offset-4 hover:[&>a]:no-underline",
|
||||
"[&>a]:text-primary [&>a]:underline [&>a]:underline-offset-4 hover:[&>a]:no-underline",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -80,15 +80,15 @@ function InputGroupAddon({
|
||||
}
|
||||
|
||||
const inputGroupButtonVariants = cva(
|
||||
"text-sm shadow-none flex gap-2 items-center",
|
||||
"shadow-none flex items-center rounded-[var(--btn-radius)]",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
|
||||
sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
|
||||
"icon-xs":
|
||||
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
||||
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
||||
xs: "h-6 gap-[var(--btn-gap-sm)] px-2 text-xs [--btn-icon-size:0.875rem] has-[>svg]:px-2",
|
||||
sm: "h-[var(--btn-height-sm)] gap-[var(--btn-gap-sm)] px-[var(--btn-px-sm)] text-sm [--btn-icon-size:0.875rem] has-[>svg]:px-[calc(var(--btn-px-sm)-0.125rem)]",
|
||||
"icon-xs": "size-6 gap-0 p-0 [--btn-icon-size:0.875rem]",
|
||||
"icon-sm":
|
||||
"size-[var(--btn-height-sm)] gap-0 p-0 [--btn-icon-size:0.875rem]",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -8,8 +8,8 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input bg-background h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base text-foreground shadow-xs transition-[color,background-color,border-color,box-shadow] duration-200 outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-surface/60",
|
||||
"focus-visible:border-primary focus-visible:ring-primary/25 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -135,7 +135,7 @@ function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
data-slot="item-description"
|
||||
className={cn(
|
||||
"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
|
||||
"[&>a]:text-accent [&>a]:underline [&>a]:underline-offset-4 hover:[&>a]:no-underline",
|
||||
"[&>a]:text-primary [&>a]:underline [&>a]:underline-offset-4 hover:[&>a]:no-underline",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -32,9 +32,9 @@ export function LogoItem({
|
||||
<div className={cn("flex items-center justify-center", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded-2xl border border-border/50 bg-white shadow-sm transition-all duration-300",
|
||||
"hover:-translate-y-1 hover:shadow-md",
|
||||
"dark:border-white/10 dark:bg-white/95 dark:shadow-lg dark:shadow-black/25",
|
||||
"flex items-center justify-center rounded-xl border border-surface bg-card shadow-glow-sm transition-[border-color,box-shadow,transform] duration-200",
|
||||
"hover:-translate-y-0.5 hover:border-primary/30 hover:shadow-glow-primary",
|
||||
"dark:bg-card dark:hover:border-primary/40",
|
||||
tileSizeMap[size]
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -75,7 +75,7 @@ function NavigationMenuTrigger({
|
||||
>
|
||||
{children}{" "}
|
||||
<ChevronDownIcon
|
||||
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
className="relative top-[1px] ms-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
@@ -90,7 +90,7 @@ function NavigationMenuContent({
|
||||
<NavigationMenuPrimitive.Content
|
||||
data-slot="navigation-menu-content"
|
||||
className={cn(
|
||||
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
||||
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 start-0 w-full p-2 pe-2.5 md:absolute md:w-auto",
|
||||
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||
className
|
||||
)}
|
||||
@@ -106,7 +106,7 @@ function NavigationMenuViewport({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-full left-0 isolate z-50 flex justify-center"
|
||||
"absolute top-full start-0 isolate z-50 flex justify-center"
|
||||
)}
|
||||
>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
|
||||
@@ -7,12 +7,13 @@ import {
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { type } from "@/lib/typography"
|
||||
|
||||
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
aria-label="صفحهبندی"
|
||||
data-slot="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
{...props}
|
||||
@@ -55,7 +56,7 @@ function PaginationLink({
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
variant: isActive ? "secondary" : "ghost",
|
||||
size,
|
||||
}),
|
||||
className
|
||||
@@ -71,13 +72,14 @@ function PaginationPrevious({
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
aria-label="صفحه قبلی"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||
className={cn("gap-1 px-2.5 sm:ps-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
<span className="hidden sm:block">Previous</span>
|
||||
{/* In RTL, previous is toward the right */}
|
||||
<ChevronRightIcon />
|
||||
<span className={cn(type.button, "hidden sm:inline")}>قبلی</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
@@ -88,13 +90,14 @@ function PaginationNext({
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
aria-label="صفحه بعدی"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||
className={cn("gap-1 px-2.5 sm:pe-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden sm:block">Next</span>
|
||||
<ChevronRightIcon />
|
||||
<span className={cn(type.button, "hidden sm:inline")}>بعدی</span>
|
||||
{/* In RTL, next is toward the left */}
|
||||
<ChevronLeftIcon />
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
@@ -111,7 +114,7 @@ function PaginationEllipsis({
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
<span className="sr-only">صفحات بیشتر</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ function SelectTrigger({
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-primary focus-visible:ring-primary/25 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-surface/60 dark:hover:bg-surface flex w-fit items-center justify-between gap-2 rounded-md border bg-background px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,background-color,border-color,box-shadow] duration-200 outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -109,12 +109,12 @@ function SelectItem({
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pe-8 ps-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<span className="absolute end-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
|
||||
@@ -70,7 +70,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
"text-foreground h-10 px-2 text-start align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pe-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -83,7 +83,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pe-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -51,8 +51,8 @@ export const BorderBeam = ({
|
||||
size = 50,
|
||||
delay = 0,
|
||||
duration = 6,
|
||||
colorFrom = "#FCC61D",
|
||||
colorTo = "#0046FF",
|
||||
colorFrom = "#29A19C",
|
||||
colorTo = "#044343",
|
||||
transition,
|
||||
style,
|
||||
reverse = false,
|
||||
|
||||
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"border-input bg-background placeholder:text-muted-foreground text-foreground focus-visible:border-primary focus-visible:ring-primary/25 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-surface/60 flex field-sizing-content min-h-16 w-full rounded-md border px-3 py-2 text-base shadow-xs transition-[color,background-color,border-color,box-shadow] duration-200 outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -17,10 +17,13 @@ const defaultElement: Record<TypographyVariant, React.ElementType> = {
|
||||
body: "p",
|
||||
"body-sm": "p",
|
||||
caption: "span",
|
||||
micro: "span",
|
||||
eyebrow: "p",
|
||||
label: "span",
|
||||
nav: "span",
|
||||
"nav-mobile": "span",
|
||||
button: "span",
|
||||
"button-lg": "span",
|
||||
"card-title": "h3",
|
||||
"card-desc": "p",
|
||||
chat: "p",
|
||||
@@ -75,6 +78,8 @@ export {
|
||||
Eyebrow,
|
||||
CardTitleText,
|
||||
CardDescText,
|
||||
ButtonText,
|
||||
ButtonTextLarge,
|
||||
};
|
||||
|
||||
function Display(props: Omit<TypographyProps, "variant">) {
|
||||
@@ -124,3 +129,11 @@ function CardTitleText(props: Omit<TypographyProps, "variant">) {
|
||||
function CardDescText(props: Omit<TypographyProps, "variant">) {
|
||||
return <Typography variant="card-desc" {...props} />;
|
||||
}
|
||||
|
||||
function ButtonText(props: Omit<TypographyProps, "variant">) {
|
||||
return <Typography variant="button" {...props} />;
|
||||
}
|
||||
|
||||
function ButtonTextLarge(props: Omit<TypographyProps, "variant">) {
|
||||
return <Typography variant="button-lg" {...props} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user