diff --git a/package.json b/package.json index ed209bf..9b00bf1 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "astro": "^6.4.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "embla-carousel-react": "^8.6.0", "lucide-react": "^1.17.0", "radix-ui": "^1.5.0", "react": "^19.2.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e50089d..b085433 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + embla-carousel-react: + specifier: ^8.6.0 + version: 8.6.0(react@19.2.7) lucide-react: specifier: ^1.17.0 version: 1.17.0(react@19.2.7) @@ -2078,6 +2081,19 @@ packages: electron-to-chromium@1.5.368: resolution: {integrity: sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==} + embla-carousel-react@8.6.0: + resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==} + peerDependencies: + react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + embla-carousel-reactive-utils@8.6.0: + resolution: {integrity: sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==} + peerDependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: + resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -5800,6 +5816,18 @@ snapshots: electron-to-chromium@1.5.368: {} + embla-carousel-react@8.6.0(react@19.2.7): + dependencies: + embla-carousel: 8.6.0 + embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0) + react: 19.2.7 + + embla-carousel-reactive-utils@8.6.0(embla-carousel@8.6.0): + dependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: {} + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} diff --git a/src/assets/cta-bg.jpg b/src/assets/cta-bg.jpg new file mode 100644 index 0000000..b58f5fd Binary files /dev/null and b/src/assets/cta-bg.jpg differ diff --git a/src/assets/image.png b/src/assets/image.png new file mode 100644 index 0000000..74b9d73 Binary files /dev/null and b/src/assets/image.png differ diff --git a/src/components/contact/ContactForm.astro b/src/components/contact/ContactForm.astro new file mode 100644 index 0000000..57802b6 --- /dev/null +++ b/src/components/contact/ContactForm.astro @@ -0,0 +1,67 @@ +--- +import { buttonVariants } from "@/components/ui/button"; +import { LucideIcon } from "@/components/ui/lucide-icon"; +import { cn } from "@/lib/utils"; +--- + +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
diff --git a/src/components/contact/SupportBanner.astro b/src/components/contact/SupportBanner.astro new file mode 100644 index 0000000..26cef33 --- /dev/null +++ b/src/components/contact/SupportBanner.astro @@ -0,0 +1,30 @@ +--- +import ActionLink from "@/components/landing/ActionLink.astro"; +import { LucideIcon } from "@/components/ui/lucide-icon"; +--- + +
+
+ + + +
+

نیاز به راهنمایی سریع دارید؟

+

+ تیم پشتیبانی ما آماده کمک به شماست. +

+
+
+ + + ارسال پیام به پشتیبانی + + +
diff --git a/src/components/landing/ActionLink.astro b/src/components/landing/ActionLink.astro new file mode 100644 index 0000000..6ffdce0 --- /dev/null +++ b/src/components/landing/ActionLink.astro @@ -0,0 +1,22 @@ +--- +import { buttonVariants } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +interface Props { + href: string; + variant?: "default" | "outline" | "secondary" | "ghost"; + size?: "default" | "lg" | "sm"; + class?: string; +} + +const { + href, + variant = "default", + size = "default", + class: className, +} = Astro.props; +--- + + + + diff --git a/src/components/landing/CtaSection.astro b/src/components/landing/CtaSection.astro new file mode 100644 index 0000000..af2866c --- /dev/null +++ b/src/components/landing/CtaSection.astro @@ -0,0 +1,69 @@ +--- +import ActionLink from "@/components/landing/ActionLink.astro"; +import { LucideIcon } from "@/components/ui/lucide-icon"; +import ctaBg from "@/assets/cta-bg.jpg"; +--- + +
+
+ + + +
+
+
+ +
+ +
+

+ از امروز باشگاه خود را حرفه‌ای‌تر مدیریت کنید +

+

+ به ۵۴ باشگاه دیگر در نکستا ملحق شوید. +

+
+
+ +
+ + شروع کنید + + + + تماس با ما + +
+
+
+
diff --git a/src/components/landing/FeaturesSection.astro b/src/components/landing/FeaturesSection.astro new file mode 100644 index 0000000..84b5950 --- /dev/null +++ b/src/components/landing/FeaturesSection.astro @@ -0,0 +1,34 @@ +--- +import SectionShell from "@/components/landing/SectionShell.astro"; +import SectionHeading from "@/components/landing/SectionHeading.astro"; +import { LucideIcon } from "@/components/ui/lucide-icon"; +import { features } from "@/lib/landing-data"; +--- + + + + +
+ { + features.map((feature) => ( +
+
+ +
+ +
+

+ {feature.title} +

+

+ {feature.description} +

+
+
+ )) + } +
+
diff --git a/src/components/landing/HeroSection.astro b/src/components/landing/HeroSection.astro new file mode 100644 index 0000000..15e5b43 --- /dev/null +++ b/src/components/landing/HeroSection.astro @@ -0,0 +1,68 @@ +--- +import ActionLink from "@/components/landing/ActionLink.astro"; +import heroImage from "@/assets/image.png"; +--- + +
+
+
+ +
+
+

+ مدیریت هوشمند باشگاه، + ساده‌تر از همیشه +

+ +

+ یک پلتفرم کامل برای مدیریت اعضا، مربیان، برنامه‌های تمرینی و تمام + فعالیت‌های باشگاه. +

+ +
+ + همین الان امتحان کن! + + + مشاهده پلن‌ها + +
+
+ +
+ + پیش‌نمایش داشبورد نکستا در موبایل +
+
+
diff --git a/src/components/landing/PricingSection.astro b/src/components/landing/PricingSection.astro new file mode 100644 index 0000000..80a3fda --- /dev/null +++ b/src/components/landing/PricingSection.astro @@ -0,0 +1,73 @@ +--- +import SectionShell from "@/components/landing/SectionShell.astro"; +import SectionHeading from "@/components/landing/SectionHeading.astro"; +import { LucideIcon } from "@/components/ui/lucide-icon"; +import { plans } from "@/lib/landing-data"; +--- + + + + +
+ { + plans.map((plan) => ( +
+ {plan.popular && ( + + محبوب‌ترین + + )} + +
+

{plan.name}

+

+ {plan.price} + / + {plan.period} +

+
+ +
    +
  • + + {plan.memberLimit} +
  • + + {plan.extras.map((feature) => ( +
  • + + + + {feature} +
  • + ))} +
+ +

+ {plan.description} +

+
+ )) + } +
+ + +
diff --git a/src/components/landing/SectionHeading.astro b/src/components/landing/SectionHeading.astro new file mode 100644 index 0000000..fbd26e7 --- /dev/null +++ b/src/components/landing/SectionHeading.astro @@ -0,0 +1,27 @@ +--- +interface Props { + title: string; + subtitle?: string; + centered?: boolean; +} + +const { title, subtitle, centered = true } = Astro.props; +--- + +
+

+ {title} +

+ { + subtitle && ( +

+ {subtitle} +

+ ) + } +
diff --git a/src/components/landing/SectionShell.astro b/src/components/landing/SectionShell.astro new file mode 100644 index 0000000..1ba4776 --- /dev/null +++ b/src/components/landing/SectionShell.astro @@ -0,0 +1,30 @@ +--- +interface Props { + variant?: "default" | "elevated"; + id?: string; + class?: string; + containerClass?: string; +} + +const { + variant = "default", + id, + class: className, + containerClass, +} = Astro.props; +--- + +
+
+ +
+
diff --git a/src/components/landing/StatsSection.astro b/src/components/landing/StatsSection.astro new file mode 100644 index 0000000..48f9833 --- /dev/null +++ b/src/components/landing/StatsSection.astro @@ -0,0 +1,34 @@ +--- +import SectionShell from "@/components/landing/SectionShell.astro"; +import { HeroSocialProof } from "@/components/landing/hero-social-proof"; +import { LucideIcon } from "@/components/ui/lucide-icon"; +import { stats } from "@/lib/landing-data"; +--- + + +
+ +
+ +
+ { + stats.map((stat) => ( +
+
+ +
+
+

{stat.value}

+

{stat.label}

+
+
+ )) + } +
+
diff --git a/src/components/landing/StepsSection.astro b/src/components/landing/StepsSection.astro new file mode 100644 index 0000000..286f261 --- /dev/null +++ b/src/components/landing/StepsSection.astro @@ -0,0 +1,36 @@ +--- +import SectionShell from "@/components/landing/SectionShell.astro"; +import SectionHeading from "@/components/landing/SectionHeading.astro"; +import { steps } from "@/lib/landing-data"; +import { cn } from "@/lib/utils"; +--- + + + + +
+ { + steps.map((step, i) => ( +
+ + {step.number} + + +
+

{step.title}

+

+ {step.description} +

+
+
+ )) + } +
+
diff --git a/src/components/landing/TestimonialsSection.astro b/src/components/landing/TestimonialsSection.astro new file mode 100644 index 0000000..f5eabcf --- /dev/null +++ b/src/components/landing/TestimonialsSection.astro @@ -0,0 +1,49 @@ +--- +import SectionShell from "@/components/landing/SectionShell.astro"; +import SectionHeading from "@/components/landing/SectionHeading.astro"; +import { TestimonialsCarousel } from "@/components/landing/testimonials-carousel"; +import { LucideIcon } from "@/components/ui/lucide-icon"; +import { testimonials } from "@/lib/landing-data"; +--- + + + + + + + + diff --git a/src/components/landing/hero-social-proof.tsx b/src/components/landing/hero-social-proof.tsx new file mode 100644 index 0000000..7a32221 --- /dev/null +++ b/src/components/landing/hero-social-proof.tsx @@ -0,0 +1,53 @@ +import { Star } from "lucide-react"; + +import { + Avatar, + AvatarFallback, + AvatarGroup, + AvatarImage, +} from "@/components/ui/avatar"; + +const avatars = [ + { src: "https://i.pravatar.cc/80?img=11", fallback: "ا" }, + { src: "https://i.pravatar.cc/80?img=12", fallback: "س" }, + { src: "https://i.pravatar.cc/80?img=13", fallback: "ر" }, + { src: "https://i.pravatar.cc/80?img=14", fallback: "ک" }, +] as const; + +export function HeroSocialProof() { + return ( +
+ + {avatars.map((avatar) => ( + + + + {avatar.fallback} + + + ))} + + +
+
+
+ {Array.from({ length: 5 }).map((_, index) => ( + + ))} +
+ 4.9/5 +
+

+ بیش از 40 باشگاه به ما اعتماد کرده‌اند +

+
+
+ ); +} diff --git a/src/components/landing/testimonials-carousel.tsx b/src/components/landing/testimonials-carousel.tsx new file mode 100644 index 0000000..7484eed --- /dev/null +++ b/src/components/landing/testimonials-carousel.tsx @@ -0,0 +1,116 @@ +import * as React from "react"; +import useEmblaCarousel from "embla-carousel-react"; +import { Star } from "lucide-react"; + +import { testimonials } from "@/lib/landing-data"; +import { cn } from "@/lib/utils"; + +type Testimonial = (typeof testimonials)[number]; + +function TestimonialCard({ item }: { item: Testimonial }) { + return ( +
+
+
+ + {item.name.charAt(0)} + +
+

{item.name}

+

{item.role}

+
+
+ +
+ {Array.from({ length: item.rating }).map((_, index) => ( + + ))} +
+
+ +

+ «{item.quote}» +

+
+ ); +} + +function CarouselDots({ + count, + selected, + onSelect, +}: { + count: number; + selected: number; + onSelect: (index: number) => void; +}) { + return ( +
+ {Array.from({ length: count }).map((_, index) => ( +
+ ); +} + +export function TestimonialsCarousel() { + const [selected, setSelected] = React.useState(0); + const [emblaRef, emblaApi] = useEmblaCarousel({ + align: "start", + loop: false, + direction: "rtl", + containScroll: "trimSnaps", + }); + + React.useEffect(() => { + if (!emblaApi) return; + + const onSelect = () => setSelected(emblaApi.selectedScrollSnap()); + + onSelect(); + emblaApi.on("select", onSelect); + emblaApi.on("reInit", onSelect); + + return () => { + emblaApi.off("select", onSelect); + emblaApi.off("reInit", onSelect); + }; + }, [emblaApi]); + + return ( +
+
+
+ {testimonials.map((item) => ( +
+ +
+ ))} +
+
+ + emblaApi?.scrollTo(index)} + /> +
+ ); +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 75b8c3d..b70830a 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -11,7 +11,7 @@ const buttonVariants = cva( variant: { default: "bg-primary text-primary-foreground hover:bg-primary/80", outline: - "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + "border-primary bg-transparent text-primary hover:border-primary hover:bg-primary/80 hover:text-primary-foreground aria-expanded:border-primary aria-expanded:bg-primary aria-expanded:text-primary-foreground", secondary: "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", ghost: diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx new file mode 100644 index 0000000..f8202e4 --- /dev/null +++ b/src/components/ui/carousel.tsx @@ -0,0 +1,240 @@ +import * as React from "react" +import useEmblaCarousel, { + type UseEmblaCarouselType, +} from "embla-carousel-react" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react" + +type CarouselApi = UseEmblaCarouselType[1] +type UseCarouselParameters = Parameters +type CarouselOptions = UseCarouselParameters[0] +type CarouselPlugin = UseCarouselParameters[1] + +type CarouselProps = { + opts?: CarouselOptions + plugins?: CarouselPlugin + orientation?: "horizontal" | "vertical" + setApi?: (api: CarouselApi) => void +} + +type CarouselContextProps = { + carouselRef: ReturnType[0] + api: ReturnType[1] + scrollPrev: () => void + scrollNext: () => void + canScrollPrev: boolean + canScrollNext: boolean +} & CarouselProps + +const CarouselContext = React.createContext(null) + +function useCarousel() { + const context = React.useContext(CarouselContext) + + if (!context) { + throw new Error("useCarousel must be used within a ") + } + + return context +} + +function Carousel({ + orientation = "horizontal", + opts, + setApi, + plugins, + className, + children, + ...props +}: React.ComponentProps<"div"> & CarouselProps) { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === "horizontal" ? "x" : "y", + }, + plugins + ) + const [canScrollPrev, setCanScrollPrev] = React.useState(false) + const [canScrollNext, setCanScrollNext] = React.useState(false) + + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) return + setCanScrollPrev(api.canScrollPrev()) + setCanScrollNext(api.canScrollNext()) + }, []) + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev() + }, [api]) + + const scrollNext = React.useCallback(() => { + api?.scrollNext() + }, [api]) + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "ArrowLeft") { + event.preventDefault() + scrollPrev() + } else if (event.key === "ArrowRight") { + event.preventDefault() + scrollNext() + } + }, + [scrollPrev, scrollNext] + ) + + React.useEffect(() => { + if (!api || !setApi) return + setApi(api) + }, [api, setApi]) + + React.useEffect(() => { + if (!api) return + onSelect(api) + api.on("reInit", onSelect) + api.on("select", onSelect) + + return () => { + api?.off("select", onSelect) + } + }, [api, onSelect]) + + return ( + +
+ {children} +
+
+ ) +} + +function CarouselContent({ className, ...props }: React.ComponentProps<"div">) { + const { carouselRef, orientation } = useCarousel() + + return ( +
+
+
+ ) +} + +function CarouselItem({ className, ...props }: React.ComponentProps<"div">) { + const { orientation } = useCarousel() + + return ( +
+ ) +} + +function CarouselPrevious({ + className, + variant = "outline", + size = "icon-sm", + ...props +}: React.ComponentProps) { + const { orientation, scrollPrev, canScrollPrev } = useCarousel() + + return ( + + ) +} + +function CarouselNext({ + className, + variant = "outline", + size = "icon-sm", + ...props +}: React.ComponentProps) { + const { orientation, scrollNext, canScrollNext } = useCarousel() + + return ( + + ) +} + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, + useCarousel, +} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..d763cd9 --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,19 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/src/components/ui/lucide-icon.tsx b/src/components/ui/lucide-icon.tsx new file mode 100644 index 0000000..2ccd3f8 --- /dev/null +++ b/src/components/ui/lucide-icon.tsx @@ -0,0 +1,64 @@ +import { + ArrowLeft, + BarChart3, + Bell, + BicepsFlexed, + Building2, + Check, + Cloud, + Dumbbell, + FileText, + Headset, + Leaf, + Mail, + MapPin, + Phone, + SportShoe, + Star, + ThumbsUp, + TrendingUp, + UserCircle, + Users, + Wallet, + type LucideIcon as LucideIconComponent, +} from "lucide-react" + +import { cn } from "@/lib/utils" + +export const landingIcons = { + arrowLeft: ArrowLeft, + mail: Mail, + phone: Phone, + mapPin: MapPin, + headset: Headset, + users: Users, + userCircle: UserCircle, + coach: Dumbbell, + biceps: BicepsFlexed, + sportShoe: SportShoe, + dumbbell: Dumbbell, + trendingUp: TrendingUp, + chart: BarChart3, + fileText: FileText, + leaf: Leaf, + wallet: Wallet, + bell: Bell, + cloud: Cloud, + building: Building2, + thumbsUp: ThumbsUp, + star: Star, + check: Check, +} as const satisfies Record + +export type LandingIconName = keyof typeof landingIcons + +type LucideIconProps = { + name: LandingIconName + className?: string +} + +export function LucideIcon({ name, className }: LucideIconProps) { + const Icon = landingIcons[name] ?? Users + + return +} diff --git a/src/components/views/Header.astro b/src/components/views/Header.astro index 76bd15b..f19c20a 100644 --- a/src/components/views/Header.astro +++ b/src/components/views/Header.astro @@ -32,7 +32,7 @@ const pathname = Astro.url.pathname; شروع کنید diff --git a/src/components/views/header-nav.tsx b/src/components/views/header-nav.tsx index f9d1b80..873697d 100644 --- a/src/components/views/header-nav.tsx +++ b/src/components/views/header-nav.tsx @@ -11,7 +11,7 @@ export function HeaderNav({ pathname = "/", className }: HeaderNavProps) { const activeTab = getActiveNavTab(pathname) return ( - + - + diff --git a/src/lib/contact-data.ts b/src/lib/contact-data.ts new file mode 100644 index 0000000..50d2c42 --- /dev/null +++ b/src/lib/contact-data.ts @@ -0,0 +1,19 @@ +export const contactDetails = [ + { + icon: "mail" as const, + label: "ایمیل ما", + value: "hello@Nxta.com", + href: "mailto:hello@Nxta.com", + }, + { + icon: "phone" as const, + label: "شماره تماس ما", + value: "09123456789", + href: "tel:09123456789", + }, + { + icon: "mapPin" as const, + label: "آدرس ما", + value: "استان، شهر، کوچه، پلاک، واحد", + }, +] as const diff --git a/src/lib/landing-data.ts b/src/lib/landing-data.ts new file mode 100644 index 0000000..9a2e79b --- /dev/null +++ b/src/lib/landing-data.ts @@ -0,0 +1,112 @@ +export const stats = [ + { value: "+۵۰", label: "مربی", icon: "biceps" }, + { value: "+۱۰۰", label: "ورزشکار", icon: "sportShoe" }, + { value: "+۵۰", label: "باشگاه فعال", icon: "dumbbell" }, + { value: "۹۸٪", label: "رضایت کاربران", icon: "star" }, +] as const; + +export const features = [ + { + title: "مدیریت اعضا", + description: "مدیریت اطلاعات اعضا، پروفایل‌ها و روند پیشرفت به سادگی.", + icon: "users", + }, + { + title: "پنل مربیان", + description: "اختصاص برنامه تمرینی و غذایی تنها در چند ثانیه.", + icon: "userCircle", + }, + { + title: "پیگیری پیشرفت", + description: "بررسی فعالیت‌ها، اهداف و عملکرد اعضا به صورت کامل.", + icon: "trendingUp", + }, + { + title: "مدیریت اشتراک‌ها", + description: "مدیریت پلن‌ها، تمدیدها و پرداخت‌ها بدون دردسر.", + icon: "fileText", + }, + { + title: "برنامه‌های غذایی", + description: "ساخت و اختصاص برنامه‌های تغذیه حرفه‌ای و شخصی‌سازی شده.", + icon: "leaf", + }, + { + title: "اعلان‌های هوشمند", + description: "اطلاع‌رسانی خودکار به مربیان و اعضا.", + icon: "bell", + }, +] as const; + +export const steps = [ + { + number: "۱", + title: "باشگاه خود را ایجاد کنید", + description: "پروفایل باشگاه را راه‌اندازی کرده و مربیان را دعوت کنید.", + }, + { + number: "۲", + title: "اعضا را مدیریت کنید", + description: "اعضا را اضافه کرده و برای آن‌ها برنامه اختصاصی تعریف کنید.", + }, + { + number: "۳", + title: "باشگاه خود را توسعه دهید", + description: "فعالیت‌ها، پیشرفت اعضا و عملکرد باشگاه را بررسی کنید.", + }, +] as const; + +export const plans = [ + { + name: "پلن حرفه‌ای", + price: "2 میلیون تومان", + period: "ماهیانه", + popular: false, + memberLimit: "تا 5 مربی و 50 ورزشکار", + extras: ["دسترسی به حرکات عمومی", "دسترسی به وعده‌های عمومی"], + description: + "مناسب برای باشگاه‌های بزرگ که به دسترسی کامل و انعطاف‌پذیری بالا نیاز دارند.", + }, + { + name: "پلن استاندارد", + price: "2 میلیون تومان", + period: "ماهیانه", + popular: true, + memberLimit: "تا 5 مربی و 50 ورزشکار", + extras: ["دسترسی به حرکات عمومی"], + description: "ایده‌آل برای باشگاه‌های در حال رشد با اعضای فعال.", + }, + { + name: "پلن ساده", + price: "2 میلیون تومان", + period: "ماهیانه", + popular: false, + memberLimit: "تا 5 مربی و 50 ورزشکار", + extras: [], + description: "بهترین انتخاب برای باشگاه‌های تازه‌به‌راه‌افتاده.", + }, +] as const; + +export const testimonials = [ + { + name: "امیر حسینی", + role: "مدیر باشگاه فیت‌پلاس", + quote: + "از وقتی نکستا را استفاده می‌کنیم، مدیریت اعضا و مالی یک‌سوم زمان قبلی طول می‌کشد. واقعاً ساده و کاربردی است.", + rating: 5, + }, + { + name: "سارا محمدی", + role: "مربی بدنسازی", + quote: + "پنل مربی عالیه. برنامه تمرینی و پیشرفت شاگردها را یکجا می‌بینم و دیگر نیازی به دفتر و اکسل نیست.", + rating: 5, + }, + { + name: "رضا کریمی", + role: "مالک باشگاه آیرون", + quote: + "گزارش‌های مالی و آماری دقیقاً همان چیزی بود که نیاز داشتم. تیم پشتیبانی هم سریع و حرفه‌ای پاسخ می‌دهند.", + rating: 5, + }, +] as const; diff --git a/src/lib/nav-links.ts b/src/lib/nav-links.ts index 7ea7784..efe15f0 100644 --- a/src/lib/nav-links.ts +++ b/src/lib/nav-links.ts @@ -18,10 +18,12 @@ export const socialLinks = [ export type HeaderNavValue = (typeof headerNavLinks)[number]["value"] -export function getActiveNavTab(pathname: string): HeaderNavValue { +export function getActiveNavTab(pathname: string): HeaderNavValue | undefined { const path = pathname.replace(/\/$/, "") || "/" + if (path === "/" || path === "") return undefined if (path === "/blog" || path.startsWith("/blog/")) return "blog" if (path === "/contact" || path.startsWith("/contact/")) return "contact" - return "plans" + if (path === "/plans" || path.startsWith("/plans/")) return "plans" + return undefined } diff --git a/src/pages/contact.astro b/src/pages/contact.astro index 463cbf0..06b9a74 100644 --- a/src/pages/contact.astro +++ b/src/pages/contact.astro @@ -1,9 +1,85 @@ --- -import Layout from "../layouts/Layout.astro"; +import Layout from "@/layouts/Layout.astro"; +import ContactForm from "@/components/contact/ContactForm.astro"; +import SupportBanner from "@/components/contact/SupportBanner.astro"; +import { LucideIcon } from "@/components/ui/lucide-icon"; +import { contactDetails } from "@/lib/contact-data"; --- -
-

تماس با ما

+
+ + +
+
+
+

+ با ما در ارتباط باشید +

+ + + +
+ { + contactDetails.map((item) => ( +
+ + + +
+

{item.label}

+ {item.href ? ( + + {item.value} + + ) : ( +

+ {item.value} +

+ )} +
+
+ )) + } +
+ +
+

+ در کنار شما هستیم +

+

+ برای دریافت راهنمایی، پشتیبانی یا اطلاعات بیشتر، فرم را تکمیل + کنید تا با شما در ارتباط باشیم. +

+
+
+ + +
+ +
+ +
+
diff --git a/src/pages/index.astro b/src/pages/index.astro index 6ce0937..df54a27 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,3 +1,20 @@ --- -return Astro.redirect("/plans"); +import Layout from "../layouts/Layout.astro"; +import HeroSection from "@/components/landing/HeroSection.astro"; +import StatsSection from "@/components/landing/StatsSection.astro"; +import FeaturesSection from "@/components/landing/FeaturesSection.astro"; +import StepsSection from "@/components/landing/StepsSection.astro"; +import PricingSection from "@/components/landing/PricingSection.astro"; +import TestimonialsSection from "@/components/landing/TestimonialsSection.astro"; +import CtaSection from "@/components/landing/CtaSection.astro"; --- + + + + + + + + + + diff --git a/src/styles/global.css b/src/styles/global.css index d151149..092135a 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -151,6 +151,29 @@ display: grid; grid-template-rows: auto 1fr auto; font-family: "iransansxv", sans-serif; + overflow-x: clip; + } + + main { + min-width: 0; + overflow-x: clip; + } +} + +@layer components { + .glass-surface { + @apply rounded-2xl border border-white/5 bg-card/15 backdrop-blur-xl; + } + + .glass-input { + @apply w-full border-0 text-sm text-muted-foreground outline-none transition-colors placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-primary/20; + background: rgb(239 238 234 / 0.06); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + } + + .contact-icon { + @apply flex shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary; } }