feat: landing & contact
This commit is contained in:
@@ -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",
|
||||
|
||||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
BIN
src/assets/cta-bg.jpg
Normal file
BIN
src/assets/cta-bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 658 KiB |
BIN
src/assets/image.png
Normal file
BIN
src/assets/image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 442 KiB |
67
src/components/contact/ContactForm.astro
Normal file
67
src/components/contact/ContactForm.astro
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { LucideIcon } from "@/components/ui/lucide-icon";
|
||||
import { cn } from "@/lib/utils";
|
||||
---
|
||||
|
||||
<form
|
||||
id="contact-form"
|
||||
class="glass-surface p-6 md:p-8"
|
||||
method="post"
|
||||
action="#"
|
||||
>
|
||||
<div class="space-y-5">
|
||||
<div class="space-y-3">
|
||||
<label for="name" class="text-sm font-medium text-foreground">نام</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="نام شما"
|
||||
class="glass-input h-11 mt-2 rounded-full px-4"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<label for="email" class="text-sm font-medium text-foreground m"
|
||||
>ایمیل</label
|
||||
>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="example@gmail.com"
|
||||
class="glass-input mt-2 h-11 rounded-full px-4"
|
||||
dir="ltr"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<label for="message" class="text-sm font-medium text-foreground"
|
||||
>متن پیام</label
|
||||
>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
rows="5"
|
||||
placeholder="پیام خود را تایپ کنید..."
|
||||
class={cn(
|
||||
"glass-input h-auto min-h-32 resize-y rounded-2xl px-4 mt-2 py-3",
|
||||
)}
|
||||
required></textarea>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class={cn(
|
||||
buttonVariants({ size: "lg" }),
|
||||
"h-11 w-full rounded-xl text-base [&_svg]:ms-1",
|
||||
)}
|
||||
>
|
||||
ارسال پیام
|
||||
<LucideIcon name="arrowLeft" className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
30
src/components/contact/SupportBanner.astro
Normal file
30
src/components/contact/SupportBanner.astro
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
import ActionLink from "@/components/landing/ActionLink.astro";
|
||||
import { LucideIcon } from "@/components/ui/lucide-icon";
|
||||
---
|
||||
|
||||
<div
|
||||
class="glass-surface flex flex-col gap-6 p-6 md:flex-row md:items-center md:justify-between md:p-8"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="contact-icon size-12 rounded-xl">
|
||||
<LucideIcon name="headset" className="size-6" />
|
||||
</span>
|
||||
<div>
|
||||
<p class="font-semibold text-foreground">نیاز به راهنمایی سریع دارید؟</p>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
تیم پشتیبانی ما آماده کمک به شماست.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ActionLink
|
||||
href="#contact-form"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
class="shrink-0 rounded-lg px-6 [&_svg]:ms-1"
|
||||
>
|
||||
ارسال پیام به پشتیبانی
|
||||
<LucideIcon name="arrowLeft" className="size-4" />
|
||||
</ActionLink>
|
||||
</div>
|
||||
22
src/components/landing/ActionLink.astro
Normal file
22
src/components/landing/ActionLink.astro
Normal file
@@ -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;
|
||||
---
|
||||
|
||||
<a href={href} class={cn(buttonVariants({ variant, size }), className)}>
|
||||
<slot />
|
||||
</a>
|
||||
69
src/components/landing/CtaSection.astro
Normal file
69
src/components/landing/CtaSection.astro
Normal file
@@ -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";
|
||||
---
|
||||
|
||||
<section class="bg-background px-4 py-10 md:px-6 md:py-12 lg:px-8">
|
||||
<div
|
||||
class="relative mx-auto max-w-7xl overflow-hidden rounded-3xl border border-border"
|
||||
>
|
||||
<img
|
||||
src={ctaBg.src}
|
||||
alt=""
|
||||
class="absolute inset-0 size-full object-cover"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 bg-black/90"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="relative flex flex-col items-center gap-6 p-6 text-center md:flex-row md:items-center md:justify-between md:gap-8 md:p-10 md:text-start lg:p-12"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-4 md:flex-row md:items-center">
|
||||
<div
|
||||
class="flex size-14 shrink-0 items-center justify-center rounded-xl bg-background md:size-16"
|
||||
>
|
||||
<img
|
||||
src="/logo.svg"
|
||||
alt=""
|
||||
class="size-8 md:size-9"
|
||||
width="36"
|
||||
height="36"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-foreground md:text-xl">
|
||||
از امروز باشگاه خود را حرفهایتر مدیریت کنید
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
به ۵۴ باشگاه دیگر در نکستا ملحق شوید.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-col gap-3 md:w-auto md:flex-row md:flex-wrap md:items-center">
|
||||
<ActionLink
|
||||
href="/plans"
|
||||
size="lg"
|
||||
class="w-full justify-center rounded-full px-6 md:w-auto [&_svg]:ms-1"
|
||||
>
|
||||
شروع کنید
|
||||
<LucideIcon name="arrowLeft" className="size-4" />
|
||||
</ActionLink>
|
||||
<ActionLink
|
||||
href="/contact"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
class="w-full justify-center rounded-full border-white/40 bg-transparent px-6 text-foreground hover:border-white/60 hover:bg-white/10 hover:text-foreground md:w-auto"
|
||||
>
|
||||
تماس با ما
|
||||
</ActionLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
34
src/components/landing/FeaturesSection.astro
Normal file
34
src/components/landing/FeaturesSection.astro
Normal file
@@ -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";
|
||||
---
|
||||
|
||||
<SectionShell id="features" variant="default">
|
||||
<SectionHeading
|
||||
title="ویژگیها"
|
||||
subtitle="هر آنچه برای مدیریت حرفهای باشگاه خود نیاز دارید"
|
||||
/>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-3">
|
||||
{
|
||||
features.map((feature) => (
|
||||
<article class="flex items-start gap-4 rounded-2xl border border-border bg-card/20 p-5 transition-colors hover:border-primary/30">
|
||||
<div class="flex size-14 shrink-0 items-center justify-center rounded-lg border border-primary">
|
||||
<LucideIcon name={feature.icon} className="size-7 text-primary" />
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex-1 space-y-1.5">
|
||||
<h3 class="text-base font-semibold text-foreground">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p class="text-sm leading-relaxed text-muted-foreground">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</SectionShell>
|
||||
68
src/components/landing/HeroSection.astro
Normal file
68
src/components/landing/HeroSection.astro
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
import ActionLink from "@/components/landing/ActionLink.astro";
|
||||
import heroImage from "@/assets/image.png";
|
||||
---
|
||||
|
||||
<section
|
||||
class="relative overflow-hidden bg-background px-4 pt-12 pb-16 md:px-6 md:pt-24 md:pb-24 lg:px-8"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,rgba(165,255,1,0.08),transparent_55%)]"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="relative mx-auto grid max-w-7xl items-center gap-10 lg:grid-cols-2 lg:gap-16"
|
||||
>
|
||||
<div class="space-y-6 text-center lg:space-y-8 lg:text-start">
|
||||
<h1
|
||||
class="text-3xl leading-tight font-bold text-primary md:text-4xl lg:text-5xl"
|
||||
>
|
||||
مدیریت هوشمند باشگاه،
|
||||
<span class="text-primary">سادهتر از همیشه</span>
|
||||
</h1>
|
||||
|
||||
<p
|
||||
class="mx-auto max-w-xl text-base leading-relaxed text-muted-foreground md:text-lg lg:mx-0"
|
||||
>
|
||||
یک پلتفرم کامل برای مدیریت اعضا، مربیان، برنامههای تمرینی و تمام
|
||||
فعالیتهای باشگاه.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:flex-wrap lg:justify-start">
|
||||
<ActionLink
|
||||
href="/plans"
|
||||
size="lg"
|
||||
class="w-full justify-center rounded-full px-6 py-5 lg:w-auto"
|
||||
>
|
||||
همین الان امتحان کن!
|
||||
</ActionLink>
|
||||
<ActionLink
|
||||
href="/plans"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
class="w-full justify-center rounded-full px-6 py-5 lg:w-auto"
|
||||
>
|
||||
مشاهده پلنها
|
||||
</ActionLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative mx-auto w-full max-w-sm md:max-w-lg lg:max-w-xl lg:ms-auto">
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_center,rgba(50,217,203,0.15),transparent_70%)]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</div>
|
||||
<img
|
||||
src={heroImage.src}
|
||||
width={heroImage.width}
|
||||
height={heroImage.height}
|
||||
alt="پیشنمایش داشبورد نکستا در موبایل"
|
||||
class="relative w-full object-contain"
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
73
src/components/landing/PricingSection.astro
Normal file
73
src/components/landing/PricingSection.astro
Normal file
@@ -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";
|
||||
---
|
||||
|
||||
<SectionShell id="pricing" variant="default">
|
||||
<SectionHeading title="پلنها" subtitle="پلنهای متناسب با نیاز باشگاه شما" />
|
||||
|
||||
<div
|
||||
class="flex w-full flex-col-reverse items-stretch gap-4 lg:grid lg:grid-cols-3 lg:gap-5"
|
||||
>
|
||||
{
|
||||
plans.map((plan) => (
|
||||
<article
|
||||
class:list={[
|
||||
"relative w-full rounded-2xl border bg-card p-6",
|
||||
plan.popular ? "border-primary" : "border-border",
|
||||
]}
|
||||
>
|
||||
{plan.popular && (
|
||||
<span class="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-primary px-4 py-1 text-xs font-medium whitespace-nowrap text-primary-foreground lg:left-auto lg:translate-x-0 lg:right-6">
|
||||
محبوبترین
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div class="mb-6 flex items-start justify-between gap-4">
|
||||
<h3 class="text-lg font-semibold text-foreground">{plan.name}</h3>
|
||||
<p class="shrink-0 text-left text-sm leading-snug text-muted-foreground">
|
||||
<span class="text-foreground text-lg">{plan.price}</span>
|
||||
<span class="mx-1 text-muted-foreground/60">/</span>
|
||||
{plan.period}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul class="flex flex-col gap-3">
|
||||
<li class="flex items-center gap-2 text-sm text-foreground">
|
||||
<LucideIcon
|
||||
name="users"
|
||||
className="size-4 shrink-0 text-primary"
|
||||
/>
|
||||
<span>{plan.memberLimit}</span>
|
||||
</li>
|
||||
|
||||
{plan.extras.map((feature) => (
|
||||
<li class="flex items-center gap-2 text-sm text-foreground">
|
||||
<span class="flex size-5 shrink-0 items-center justify-center rounded-full border border-primary">
|
||||
<LucideIcon name="check" className="size-3 text-primary" />
|
||||
</span>
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<p class="mt-6 text-sm leading-relaxed text-muted-foreground">
|
||||
{plan.description}
|
||||
</p>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-center md:mt-8 md:justify-end">
|
||||
<a
|
||||
href="/plans"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-primary transition-colors hover:text-primary/80"
|
||||
>
|
||||
مشاهده و خرید
|
||||
<span aria-hidden="true">←</span>
|
||||
</a>
|
||||
</div>
|
||||
</SectionShell>
|
||||
27
src/components/landing/SectionHeading.astro
Normal file
27
src/components/landing/SectionHeading.astro
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
interface Props {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
centered?: boolean;
|
||||
}
|
||||
|
||||
const { title, subtitle, centered = true } = Astro.props;
|
||||
---
|
||||
|
||||
<div class:list={["mb-8 md:mb-10 lg:mb-14", centered && "text-center"]}>
|
||||
<h2 class="text-base font-bold text-primary md:text-xl lg:text-2xl">
|
||||
{title}
|
||||
</h2>
|
||||
{
|
||||
subtitle && (
|
||||
<p
|
||||
class:list={[
|
||||
"mt-2 max-w-3xl font-semibold text-foreground md:mt-3 md:text-2xl lg:text-3xl",
|
||||
centered && "mx-auto",
|
||||
]}
|
||||
>
|
||||
{subtitle}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
30
src/components/landing/SectionShell.astro
Normal file
30
src/components/landing/SectionShell.astro
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
interface Props {
|
||||
variant?: "default" | "elevated";
|
||||
id?: string;
|
||||
class?: string;
|
||||
containerClass?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
variant = "default",
|
||||
id,
|
||||
class: className,
|
||||
containerClass,
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<section
|
||||
id={id}
|
||||
class:list={[
|
||||
"px-4 py-12 md:px-6 md:py-16 lg:px-8 lg:py-24",
|
||||
variant === "elevated"
|
||||
? "border-y border-border bg-card"
|
||||
: "bg-background",
|
||||
className,
|
||||
]}
|
||||
>
|
||||
<div class:list={["mx-auto max-w-7xl", containerClass]}>
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
34
src/components/landing/StatsSection.astro
Normal file
34
src/components/landing/StatsSection.astro
Normal file
@@ -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";
|
||||
---
|
||||
|
||||
<SectionShell
|
||||
variant="default"
|
||||
class="pt-2 pb-10 md:pt-3 md:pb-16"
|
||||
containerClass="flex flex-col gap-4 md:gap-3"
|
||||
>
|
||||
<div
|
||||
class="flex justify-center pb-2 md:justify-end md:pb-3 lg:max-w-xl lg:ms-auto"
|
||||
>
|
||||
<HeroSocialProof />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3 md:gap-4 lg:grid-cols-4">
|
||||
{
|
||||
stats.map((stat) => (
|
||||
<article class="flex items-center gap-3 rounded-2xl border border-border bg-card/20 p-4 sm:gap-4 sm:p-5">
|
||||
<div class="flex size-12 shrink-0 items-center justify-center rounded-xl bg-primary/10 sm:size-16">
|
||||
<LucideIcon name={stat.icon} className="size-6 text-primary sm:size-8" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-xl font-bold text-primary sm:text-2xl">{stat.value}</p>
|
||||
<p class="text-xs text-muted-foreground sm:text-sm">{stat.label}</p>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</SectionShell>
|
||||
36
src/components/landing/StepsSection.astro
Normal file
36
src/components/landing/StepsSection.astro
Normal file
@@ -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";
|
||||
---
|
||||
|
||||
<SectionShell variant="default">
|
||||
<SectionHeading title="ساده برای شروع، قدرتمند در اجرا" subtitle="" />
|
||||
|
||||
<div class="flex flex-col gap-4 md:grid md:grid-cols-3 md:gap-0">
|
||||
{
|
||||
steps.map((step, i) => (
|
||||
<article
|
||||
class={cn(
|
||||
"relative flex items-start gap-4 border border-border bg-background p-5 md:p-6",
|
||||
"rounded-2xl md:rounded-none",
|
||||
i === 0 && "md:rounded-tr-2xl md:rounded-br-2xl",
|
||||
i === steps.length - 1 && "md:rounded-tl-2xl md:rounded-bl-2xl",
|
||||
)}
|
||||
>
|
||||
<span class="flex size-12 shrink-0 items-center justify-center rounded-full bg-primary text-2xl font-bold text-primary-foreground md:size-14 md:text-3xl">
|
||||
{step.number}
|
||||
</span>
|
||||
|
||||
<div class="min-w-0 flex-1 space-y-1.5">
|
||||
<h3 class="text-lg font-semibold text-foreground">{step.title}</h3>
|
||||
<p class="text-sm leading-relaxed text-muted-foreground">
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</SectionShell>
|
||||
49
src/components/landing/TestimonialsSection.astro
Normal file
49
src/components/landing/TestimonialsSection.astro
Normal file
@@ -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";
|
||||
---
|
||||
|
||||
<SectionShell variant="default" containerClass="min-w-0 overflow-x-clip">
|
||||
<SectionHeading
|
||||
title="نظرات کاربران"
|
||||
subtitle="مورد اعتماد مدیران باشگاه."
|
||||
/>
|
||||
|
||||
<TestimonialsCarousel client:load />
|
||||
|
||||
<div class="hidden gap-6 md:grid md:grid-cols-3">
|
||||
{
|
||||
testimonials.map((item) => (
|
||||
<article class="flex flex-col rounded-2xl border border-border bg-card p-6">
|
||||
<div class="mb-4 flex items-start justify-between gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="flex size-14 shrink-0 items-center justify-center rounded-full bg-primary/10 text-lg font-bold text-primary">
|
||||
{item.name.charAt(0)}
|
||||
</span>
|
||||
<div>
|
||||
<p class="font-medium text-foreground">{item.name}</p>
|
||||
<p class="text-xs text-muted-foreground">{item.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 gap-0.5">
|
||||
{Array.from({ length: item.rating }).map(() => (
|
||||
<LucideIcon
|
||||
name="star"
|
||||
className="size-4 fill-yellow-400 text-yellow-400"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm leading-relaxed text-muted-foreground">
|
||||
«{item.quote}»
|
||||
</p>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</SectionShell>
|
||||
53
src/components/landing/hero-social-proof.tsx
Normal file
53
src/components/landing/hero-social-proof.tsx
Normal file
@@ -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 (
|
||||
<div
|
||||
dir="ltr"
|
||||
className="flex w-full max-w-sm flex-col items-center gap-3 sm:max-w-none sm:flex-row sm:items-center sm:gap-4"
|
||||
>
|
||||
<AvatarGroup className="shrink-0">
|
||||
{avatars.map((avatar) => (
|
||||
<Avatar key={avatar.src} size="lg">
|
||||
<AvatarImage src={avatar.src} alt="" />
|
||||
<AvatarFallback className="bg-card text-xs text-primary">
|
||||
{avatar.fallback}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
))}
|
||||
</AvatarGroup>
|
||||
|
||||
<div className="flex flex-col items-center gap-1 text-center sm:items-start sm:text-start">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-0.5">
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<Star
|
||||
key={index}
|
||||
className="size-4 fill-yellow-400 text-yellow-400"
|
||||
aria-hidden
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">4.9/5</span>
|
||||
</div>
|
||||
<p className="text-sm text-foreground">
|
||||
بیش از 40 باشگاه به ما اعتماد کردهاند
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
116
src/components/landing/testimonials-carousel.tsx
Normal file
116
src/components/landing/testimonials-carousel.tsx
Normal file
@@ -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 (
|
||||
<article className="flex min-w-0 flex-col overflow-hidden rounded-2xl border border-border bg-card p-6">
|
||||
<div className="mb-4 flex min-w-0 items-start justify-between gap-4">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<span className="flex size-14 shrink-0 items-center justify-center rounded-full bg-primary/10 text-lg font-bold text-primary">
|
||||
{item.name.charAt(0)}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-medium text-foreground">{item.name}</p>
|
||||
<p className="truncate text-xs text-muted-foreground">{item.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 gap-0.5">
|
||||
{Array.from({ length: item.rating }).map((_, index) => (
|
||||
<Star
|
||||
key={index}
|
||||
className="size-4 fill-yellow-400 text-yellow-400"
|
||||
aria-hidden
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="break-words text-sm leading-relaxed text-muted-foreground">
|
||||
«{item.quote}»
|
||||
</p>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselDots({
|
||||
count,
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
count: number;
|
||||
selected: number;
|
||||
onSelect: (index: number) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex justify-center gap-2 pt-5">
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
aria-label={`نظر ${index + 1} از ${count}`}
|
||||
aria-current={index === selected ? "true" : undefined}
|
||||
onClick={() => onSelect(index)}
|
||||
className={cn(
|
||||
"size-2 rounded-full transition-colors",
|
||||
index === selected ? "bg-primary" : "bg-muted-foreground",
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="w-full max-w-full min-w-0 md:hidden">
|
||||
<div ref={emblaRef} className="w-full max-w-full overflow-hidden">
|
||||
<div className="flex touch-pan-y">
|
||||
{testimonials.map((item) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className="min-w-0 shrink-0 grow-0 basis-full"
|
||||
>
|
||||
<TestimonialCard item={item} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CarouselDots
|
||||
count={testimonials.length}
|
||||
selected={selected}
|
||||
onSelect={(index) => emblaApi?.scrollTo(index)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
240
src/components/ui/carousel.tsx
Normal file
240
src/components/ui/carousel.tsx
Normal file
@@ -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<typeof useEmblaCarousel>
|
||||
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<typeof useEmblaCarousel>[0]
|
||||
api: ReturnType<typeof useEmblaCarousel>[1]
|
||||
scrollPrev: () => void
|
||||
scrollNext: () => void
|
||||
canScrollPrev: boolean
|
||||
canScrollNext: boolean
|
||||
} & CarouselProps
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />")
|
||||
}
|
||||
|
||||
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<HTMLDivElement>) => {
|
||||
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 (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn("relative", className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
data-slot="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { carouselRef, orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={carouselRef}
|
||||
className="overflow-hidden"
|
||||
data-slot="carousel-content"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
data-slot="carousel-item"
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselPrevious({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon-sm",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-slot="carousel-previous"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute touch-manipulation rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -left-12 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselNext({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon-sm",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-slot="carousel-next"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute touch-manipulation rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -right-12 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ChevronRightIcon />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
type CarouselApi,
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
useCarousel,
|
||||
}
|
||||
19
src/components/ui/input.tsx
Normal file
19
src/components/ui/input.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
64
src/components/ui/lucide-icon.tsx
Normal file
64
src/components/ui/lucide-icon.tsx
Normal file
@@ -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<string, LucideIconComponent>
|
||||
|
||||
export type LandingIconName = keyof typeof landingIcons
|
||||
|
||||
type LucideIconProps = {
|
||||
name: LandingIconName
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function LucideIcon({ name, className }: LucideIconProps) {
|
||||
const Icon = landingIcons[name] ?? Users
|
||||
|
||||
return <Icon className={cn("size-6 shrink-0", className)} aria-hidden />
|
||||
}
|
||||
@@ -32,7 +32,7 @@ const pathname = Astro.url.pathname;
|
||||
|
||||
<Link
|
||||
href="/plans"
|
||||
className="hidden shrink-0 rounded-full px-6 md:inline-flex lg:px-8"
|
||||
className="hidden shrink-0 rounded-full px-6 py-5 md:inline-flex lg:px-8"
|
||||
>
|
||||
شروع کنید
|
||||
</Link>
|
||||
|
||||
@@ -11,7 +11,7 @@ export function HeaderNav({ pathname = "/", className }: HeaderNavProps) {
|
||||
const activeTab = getActiveNavTab(pathname)
|
||||
|
||||
return (
|
||||
<Tabs value={activeTab} className={cn("gap-0", className)}>
|
||||
<Tabs value={activeTab ?? ""} className={cn("gap-0", className)}>
|
||||
<TabsList
|
||||
variant="line"
|
||||
className="h-auto gap-3 bg-transparent p-0 pb-1 sm:gap-5 md:gap-8"
|
||||
|
||||
@@ -14,7 +14,7 @@ const { title = "نکستا" } = Astro.props;
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/font/font.css" />
|
||||
|
||||
19
src/lib/contact-data.ts
Normal file
19
src/lib/contact-data.ts
Normal file
@@ -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
|
||||
112
src/lib/landing-data.ts
Normal file
112
src/lib/landing-data.ts
Normal file
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
---
|
||||
|
||||
<Layout title="تماس با ما | نکستا">
|
||||
<section class="mx-auto max-w-7xl px-6 py-16 lg:px-8">
|
||||
<h1 class="text-3xl font-bold text-foreground">تماس با ما</h1>
|
||||
<section
|
||||
class="relative overflow-hidden bg-background px-4 py-16 md:px-6 md:py-20 lg:px-8"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_center,rgba(50,217,203,0.08),transparent_65%)]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="relative mx-auto max-w-7xl">
|
||||
<div class="grid gap-10 lg:grid-cols-2 lg:gap-16">
|
||||
<div class="space-y-8">
|
||||
<h1 class="text-3xl font-bold text-foreground lg:hidden">
|
||||
با ما در <span class="text-primary">ارتباط</span> باشید
|
||||
</h1>
|
||||
|
||||
<div class="hidden space-y-4 lg:block">
|
||||
<h1 class="text-3xl font-bold text-foreground md:text-4xl">
|
||||
در کنار <span class="text-primary">شما</span> هستیم
|
||||
</h1>
|
||||
<p
|
||||
class="max-w-lg text-sm leading-relaxed text-muted-foreground md:text-base"
|
||||
>
|
||||
هر سوال یا درخواستی دارید، تیم ما آماده پاسخگویی است. فرم را
|
||||
پر کنید یا از راههای ارتباطی زیر با ما در تماس باشید.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="glass-surface space-y-4 p-6">
|
||||
{
|
||||
contactDetails.map((item) => (
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="contact-icon size-11">
|
||||
<LucideIcon name={item.icon} className="size-5" />
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm text-muted-foreground">{item.label}</p>
|
||||
{item.href ? (
|
||||
<a
|
||||
href={item.href}
|
||||
class="mt-0.5 block font-medium text-foreground transition-colors hover:text-primary"
|
||||
dir={item.icon === "phone" ? "ltr" : undefined}
|
||||
>
|
||||
{item.value}
|
||||
</a>
|
||||
) : (
|
||||
<p class="mt-0.5 font-medium text-foreground">
|
||||
{item.value}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 lg:hidden">
|
||||
<h2 class="text-3xl font-bold text-foreground">
|
||||
در کنار <span class="text-primary">شما</span> هستیم
|
||||
</h2>
|
||||
<p class="text-sm leading-relaxed text-muted-foreground">
|
||||
برای دریافت راهنمایی، پشتیبانی یا اطلاعات بیشتر، فرم را تکمیل
|
||||
کنید تا با شما در ارتباط باشیم.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ContactForm />
|
||||
</div>
|
||||
|
||||
<div class="mt-12 md:mt-16">
|
||||
<SupportBanner />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
@@ -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";
|
||||
---
|
||||
|
||||
<Layout title="نکستا | مدیریت هوشمند باشگاه">
|
||||
<HeroSection />
|
||||
<StatsSection />
|
||||
<FeaturesSection />
|
||||
<StepsSection />
|
||||
<PricingSection />
|
||||
<TestimonialsSection />
|
||||
<CtaSection />
|
||||
</Layout>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user