@lukoweb/apitogo-module-landing 0.2.23 → 0.2.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/DefaultLandingPage.tsx +51 -51
- package/src/LandingFooterSlot.tsx +21 -21
- package/src/LandingShell.tsx +73 -73
- package/src/hooks/useGatewayActions.ts +131 -131
- package/src/index.tsx +42 -42
- package/src/landing.css +14 -14
- package/src/landingModule.tsx +46 -46
- package/src/sections/ApiShowcaseSection.tsx +131 -131
- package/src/sections/CodeExampleCard.tsx +37 -37
- package/src/sections/FeaturesSection.tsx +60 -60
- package/src/sections/FooterSection.tsx +161 -161
- package/src/sections/HeroSection.tsx +135 -135
- package/src/sections/LandingButtons.tsx +46 -46
- package/src/sections/LandingIcon.tsx +54 -54
- package/src/sections/PlansPreviewSection.tsx +76 -76
- package/src/sections/PricingCtaSection.tsx +82 -82
- package/src/sections/StepsSection.tsx +44 -44
- package/src/sections/TrustSection.tsx +32 -32
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import { cn } from "@lukoweb/apitogo";
|
|
2
|
-
import { Link } from "@lukoweb/apitogo/components";
|
|
3
|
-
import { Button } from "@lukoweb/apitogo/ui/Button.js";
|
|
4
|
-
import type { LucideIcon } from "lucide-react";
|
|
5
|
-
import type { ReactNode } from "react";
|
|
6
|
-
|
|
7
|
-
type LandingButtonProps = {
|
|
8
|
-
href: string;
|
|
9
|
-
label: string;
|
|
10
|
-
icon?: LucideIcon;
|
|
11
|
-
iconPosition?: "start" | "end";
|
|
12
|
-
variant?: "primary" | "secondary";
|
|
13
|
-
size?: "default" | "sm";
|
|
14
|
-
className?: string;
|
|
15
|
-
children?: ReactNode;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export const LandingButton = ({
|
|
19
|
-
href,
|
|
20
|
-
label,
|
|
21
|
-
icon: Icon,
|
|
22
|
-
iconPosition = "end",
|
|
23
|
-
variant = "primary",
|
|
24
|
-
size = "default",
|
|
25
|
-
className,
|
|
26
|
-
children,
|
|
27
|
-
}: LandingButtonProps) => {
|
|
28
|
-
const content = children ?? (
|
|
29
|
-
<>
|
|
30
|
-
{Icon && iconPosition === "start" ? <Icon size={17} aria-hidden /> : null}
|
|
31
|
-
{label}
|
|
32
|
-
{Icon && iconPosition === "end" ? <Icon size={17} aria-hidden /> : null}
|
|
33
|
-
</>
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<Button
|
|
38
|
-
asChild
|
|
39
|
-
variant={variant === "primary" ? "default" : "outline"}
|
|
40
|
-
size={size === "sm" ? "sm" : "lg"}
|
|
41
|
-
className={cn(className)}
|
|
42
|
-
>
|
|
43
|
-
<Link to={href}>{content}</Link>
|
|
44
|
-
</Button>
|
|
45
|
-
);
|
|
46
|
-
};
|
|
1
|
+
import { cn } from "@lukoweb/apitogo";
|
|
2
|
+
import { Link } from "@lukoweb/apitogo/components";
|
|
3
|
+
import { Button } from "@lukoweb/apitogo/ui/Button.js";
|
|
4
|
+
import type { LucideIcon } from "lucide-react";
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
|
+
|
|
7
|
+
type LandingButtonProps = {
|
|
8
|
+
href: string;
|
|
9
|
+
label: string;
|
|
10
|
+
icon?: LucideIcon;
|
|
11
|
+
iconPosition?: "start" | "end";
|
|
12
|
+
variant?: "primary" | "secondary";
|
|
13
|
+
size?: "default" | "sm";
|
|
14
|
+
className?: string;
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const LandingButton = ({
|
|
19
|
+
href,
|
|
20
|
+
label,
|
|
21
|
+
icon: Icon,
|
|
22
|
+
iconPosition = "end",
|
|
23
|
+
variant = "primary",
|
|
24
|
+
size = "default",
|
|
25
|
+
className,
|
|
26
|
+
children,
|
|
27
|
+
}: LandingButtonProps) => {
|
|
28
|
+
const content = children ?? (
|
|
29
|
+
<>
|
|
30
|
+
{Icon && iconPosition === "start" ? <Icon size={17} aria-hidden /> : null}
|
|
31
|
+
{label}
|
|
32
|
+
{Icon && iconPosition === "end" ? <Icon size={17} aria-hidden /> : null}
|
|
33
|
+
</>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Button
|
|
38
|
+
asChild
|
|
39
|
+
variant={variant === "primary" ? "default" : "outline"}
|
|
40
|
+
size={size === "sm" ? "sm" : "lg"}
|
|
41
|
+
className={cn(className)}
|
|
42
|
+
>
|
|
43
|
+
<Link to={href}>{content}</Link>
|
|
44
|
+
</Button>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
import type { LucideIcon } from "lucide-react";
|
|
2
|
-
import {
|
|
3
|
-
ActivityIcon,
|
|
4
|
-
ArrowRightLeftIcon,
|
|
5
|
-
BadgeDollarSignIcon,
|
|
6
|
-
BlocksIcon,
|
|
7
|
-
BookOpenIcon,
|
|
8
|
-
BoxesIcon,
|
|
9
|
-
CheckIcon,
|
|
10
|
-
CodeIcon,
|
|
11
|
-
CoinsIcon,
|
|
12
|
-
GaugeIcon,
|
|
13
|
-
LayersIcon,
|
|
14
|
-
RadioIcon,
|
|
15
|
-
ShieldCheckIcon,
|
|
16
|
-
WalletIcon,
|
|
17
|
-
WebhookIcon,
|
|
18
|
-
ZapIcon,
|
|
19
|
-
} from "lucide-react";
|
|
20
|
-
|
|
21
|
-
const ICONS: Record<string, LucideIcon> = {
|
|
22
|
-
activity: ActivityIcon,
|
|
23
|
-
"arrow-left-right": ArrowRightLeftIcon,
|
|
24
|
-
"arrow-right-left": ArrowRightLeftIcon,
|
|
25
|
-
"badge-dollar-sign": BadgeDollarSignIcon,
|
|
26
|
-
blocks: BlocksIcon,
|
|
27
|
-
"book-open": BookOpenIcon,
|
|
28
|
-
boxes: BoxesIcon,
|
|
29
|
-
check: CheckIcon,
|
|
30
|
-
code: CodeIcon,
|
|
31
|
-
coins: CoinsIcon,
|
|
32
|
-
gauge: GaugeIcon,
|
|
33
|
-
layers: LayersIcon,
|
|
34
|
-
radio: RadioIcon,
|
|
35
|
-
"shield-check": ShieldCheckIcon,
|
|
36
|
-
wallet: WalletIcon,
|
|
37
|
-
webhook: WebhookIcon,
|
|
38
|
-
zap: ZapIcon,
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export type LandingIconProps = {
|
|
42
|
-
name?: string;
|
|
43
|
-
size?: number;
|
|
44
|
-
className?: string;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export const LandingIcon = ({
|
|
48
|
-
name,
|
|
49
|
-
size = 20,
|
|
50
|
-
className,
|
|
51
|
-
}: LandingIconProps) => {
|
|
52
|
-
const Icon = (name && ICONS[name]) || CodeIcon;
|
|
53
|
-
return <Icon size={size} className={className} aria-hidden />;
|
|
54
|
-
};
|
|
1
|
+
import type { LucideIcon } from "lucide-react";
|
|
2
|
+
import {
|
|
3
|
+
ActivityIcon,
|
|
4
|
+
ArrowRightLeftIcon,
|
|
5
|
+
BadgeDollarSignIcon,
|
|
6
|
+
BlocksIcon,
|
|
7
|
+
BookOpenIcon,
|
|
8
|
+
BoxesIcon,
|
|
9
|
+
CheckIcon,
|
|
10
|
+
CodeIcon,
|
|
11
|
+
CoinsIcon,
|
|
12
|
+
GaugeIcon,
|
|
13
|
+
LayersIcon,
|
|
14
|
+
RadioIcon,
|
|
15
|
+
ShieldCheckIcon,
|
|
16
|
+
WalletIcon,
|
|
17
|
+
WebhookIcon,
|
|
18
|
+
ZapIcon,
|
|
19
|
+
} from "lucide-react";
|
|
20
|
+
|
|
21
|
+
const ICONS: Record<string, LucideIcon> = {
|
|
22
|
+
activity: ActivityIcon,
|
|
23
|
+
"arrow-left-right": ArrowRightLeftIcon,
|
|
24
|
+
"arrow-right-left": ArrowRightLeftIcon,
|
|
25
|
+
"badge-dollar-sign": BadgeDollarSignIcon,
|
|
26
|
+
blocks: BlocksIcon,
|
|
27
|
+
"book-open": BookOpenIcon,
|
|
28
|
+
boxes: BoxesIcon,
|
|
29
|
+
check: CheckIcon,
|
|
30
|
+
code: CodeIcon,
|
|
31
|
+
coins: CoinsIcon,
|
|
32
|
+
gauge: GaugeIcon,
|
|
33
|
+
layers: LayersIcon,
|
|
34
|
+
radio: RadioIcon,
|
|
35
|
+
"shield-check": ShieldCheckIcon,
|
|
36
|
+
wallet: WalletIcon,
|
|
37
|
+
webhook: WebhookIcon,
|
|
38
|
+
zap: ZapIcon,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type LandingIconProps = {
|
|
42
|
+
name?: string;
|
|
43
|
+
size?: number;
|
|
44
|
+
className?: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const LandingIcon = ({
|
|
48
|
+
name,
|
|
49
|
+
size = 20,
|
|
50
|
+
className,
|
|
51
|
+
}: LandingIconProps) => {
|
|
52
|
+
const Icon = (name && ICONS[name]) || CodeIcon;
|
|
53
|
+
return <Icon size={size} className={className} aria-hidden />;
|
|
54
|
+
};
|
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
import { cn } from "@lukoweb/apitogo";
|
|
2
|
-
import { Link } from "@lukoweb/apitogo/components";
|
|
3
|
-
import { ArrowRightIcon } from "lucide-react";
|
|
4
|
-
|
|
5
|
-
export type PlansPreviewPlan = {
|
|
6
|
-
name: string;
|
|
7
|
-
price: string;
|
|
8
|
-
unit: string;
|
|
9
|
-
blurb: string;
|
|
10
|
-
href?: string;
|
|
11
|
-
featured?: boolean;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export type PlansPreviewSectionProps = {
|
|
15
|
-
title?: string;
|
|
16
|
-
seeAllHref?: string;
|
|
17
|
-
seeAllLabel?: string;
|
|
18
|
-
plans: PlansPreviewPlan[];
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export const PlansPreviewSection = ({
|
|
22
|
-
title = "Simple, usage-based pricing",
|
|
23
|
-
seeAllHref = "/pricing",
|
|
24
|
-
seeAllLabel = "See full pricing →",
|
|
25
|
-
plans,
|
|
26
|
-
}: PlansPreviewSectionProps) => {
|
|
27
|
-
if (plans.length === 0) {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<section className="border-t border-border bg-muted/30">
|
|
33
|
-
<div className="mx-auto max-w-[1200px] px-6 py-[3.75rem]">
|
|
34
|
-
<div className="mb-10 flex flex-wrap items-end justify-between gap-4">
|
|
35
|
-
<h2 className="text-[2.125rem] leading-tight font-bold tracking-[-0.025em]">
|
|
36
|
-
{title}
|
|
37
|
-
</h2>
|
|
38
|
-
<Link
|
|
39
|
-
to={seeAllHref}
|
|
40
|
-
className="inline-flex items-center gap-1 text-sm font-semibold text-primary no-underline hover:underline"
|
|
41
|
-
>
|
|
42
|
-
{seeAllLabel}
|
|
43
|
-
{!seeAllLabel.includes("→") ? (
|
|
44
|
-
<ArrowRightIcon size={14} aria-hidden />
|
|
45
|
-
) : null}
|
|
46
|
-
</Link>
|
|
47
|
-
</div>
|
|
48
|
-
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
|
49
|
-
{plans.map((plan) => (
|
|
50
|
-
<article
|
|
51
|
-
key={plan.name}
|
|
52
|
-
className={cn(
|
|
53
|
-
"rounded-[14px] border border-border bg-card p-6",
|
|
54
|
-
plan.featured &&
|
|
55
|
-
"border-primary/50 ring-1 ring-primary/25 shadow-[0_10px_28px_rgba(13,148,136,0.18)]",
|
|
56
|
-
)}
|
|
57
|
-
>
|
|
58
|
-
<div className="text-sm font-bold uppercase tracking-[0.04em]">
|
|
59
|
-
{plan.name}
|
|
60
|
-
</div>
|
|
61
|
-
<div className="mt-2.5 text-[1.8rem] font-extrabold tracking-[-0.02em]">
|
|
62
|
-
{plan.price}
|
|
63
|
-
</div>
|
|
64
|
-
<div className="mb-3.5 text-[0.78125rem] text-muted-foreground">
|
|
65
|
-
{plan.unit}
|
|
66
|
-
</div>
|
|
67
|
-
<p className="text-[0.8125rem] leading-relaxed text-muted-foreground">
|
|
68
|
-
{plan.blurb}
|
|
69
|
-
</p>
|
|
70
|
-
</article>
|
|
71
|
-
))}
|
|
72
|
-
</div>
|
|
73
|
-
</div>
|
|
74
|
-
</section>
|
|
75
|
-
);
|
|
76
|
-
};
|
|
1
|
+
import { cn } from "@lukoweb/apitogo";
|
|
2
|
+
import { Link } from "@lukoweb/apitogo/components";
|
|
3
|
+
import { ArrowRightIcon } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
export type PlansPreviewPlan = {
|
|
6
|
+
name: string;
|
|
7
|
+
price: string;
|
|
8
|
+
unit: string;
|
|
9
|
+
blurb: string;
|
|
10
|
+
href?: string;
|
|
11
|
+
featured?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type PlansPreviewSectionProps = {
|
|
15
|
+
title?: string;
|
|
16
|
+
seeAllHref?: string;
|
|
17
|
+
seeAllLabel?: string;
|
|
18
|
+
plans: PlansPreviewPlan[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const PlansPreviewSection = ({
|
|
22
|
+
title = "Simple, usage-based pricing",
|
|
23
|
+
seeAllHref = "/pricing",
|
|
24
|
+
seeAllLabel = "See full pricing →",
|
|
25
|
+
plans,
|
|
26
|
+
}: PlansPreviewSectionProps) => {
|
|
27
|
+
if (plans.length === 0) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<section className="border-t border-border bg-muted/30">
|
|
33
|
+
<div className="mx-auto max-w-[1200px] px-6 py-[3.75rem]">
|
|
34
|
+
<div className="mb-10 flex flex-wrap items-end justify-between gap-4">
|
|
35
|
+
<h2 className="text-[2.125rem] leading-tight font-bold tracking-[-0.025em]">
|
|
36
|
+
{title}
|
|
37
|
+
</h2>
|
|
38
|
+
<Link
|
|
39
|
+
to={seeAllHref}
|
|
40
|
+
className="inline-flex items-center gap-1 text-sm font-semibold text-primary no-underline hover:underline"
|
|
41
|
+
>
|
|
42
|
+
{seeAllLabel}
|
|
43
|
+
{!seeAllLabel.includes("→") ? (
|
|
44
|
+
<ArrowRightIcon size={14} aria-hidden />
|
|
45
|
+
) : null}
|
|
46
|
+
</Link>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
|
49
|
+
{plans.map((plan) => (
|
|
50
|
+
<article
|
|
51
|
+
key={plan.name}
|
|
52
|
+
className={cn(
|
|
53
|
+
"rounded-[14px] border border-border bg-card p-6",
|
|
54
|
+
plan.featured &&
|
|
55
|
+
"border-primary/50 ring-1 ring-primary/25 shadow-[0_10px_28px_rgba(13,148,136,0.18)]",
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
<div className="text-sm font-bold uppercase tracking-[0.04em]">
|
|
59
|
+
{plan.name}
|
|
60
|
+
</div>
|
|
61
|
+
<div className="mt-2.5 text-[1.8rem] font-extrabold tracking-[-0.02em]">
|
|
62
|
+
{plan.price}
|
|
63
|
+
</div>
|
|
64
|
+
<div className="mb-3.5 text-[0.78125rem] text-muted-foreground">
|
|
65
|
+
{plan.unit}
|
|
66
|
+
</div>
|
|
67
|
+
<p className="text-[0.8125rem] leading-relaxed text-muted-foreground">
|
|
68
|
+
{plan.blurb}
|
|
69
|
+
</p>
|
|
70
|
+
</article>
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</section>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
2
|
-
import { ArrowRightIcon } from "lucide-react";
|
|
3
|
-
import { LandingButton } from "./LandingButtons.js";
|
|
4
|
-
|
|
5
|
-
export type PricingCtaSectionProps = {
|
|
6
|
-
pricingCta: NonNullable<ResolvedLandingContent["pricingCta"]>;
|
|
7
|
-
variant?: "default" | "banner";
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export const PricingCtaSection = ({
|
|
11
|
-
pricingCta,
|
|
12
|
-
variant = "default",
|
|
13
|
-
}: PricingCtaSectionProps) => {
|
|
14
|
-
const title = pricingCta.title ?? "Start free. Scale when you do.";
|
|
15
|
-
const description =
|
|
16
|
-
pricingCta.description ??
|
|
17
|
-
"Generous free tier. Upgrade only when your app takes off.";
|
|
18
|
-
|
|
19
|
-
if (variant === "banner") {
|
|
20
|
-
return (
|
|
21
|
-
<section className="relative overflow-hidden bg-foreground text-background">
|
|
22
|
-
<div
|
|
23
|
-
aria-hidden
|
|
24
|
-
className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_50%_120%,color-mix(in_oklch,var(--primary)_30%,transparent),transparent_55%)]"
|
|
25
|
-
/>
|
|
26
|
-
<div className="relative mx-auto max-w-[640px] px-6 py-[4.5rem] text-center">
|
|
27
|
-
<h2 className="mb-3.5 text-[2.125rem] leading-tight font-bold tracking-[-0.025em] text-balance">
|
|
28
|
-
{title}
|
|
29
|
-
</h2>
|
|
30
|
-
<p className="mx-auto mb-7 max-w-[520px] text-lg leading-relaxed text-background/70">
|
|
31
|
-
{description}
|
|
32
|
-
</p>
|
|
33
|
-
<div className="flex flex-wrap items-center justify-center gap-3">
|
|
34
|
-
{pricingCta.primaryAction ? (
|
|
35
|
-
<LandingButton
|
|
36
|
-
href={pricingCta.primaryAction.href}
|
|
37
|
-
label={pricingCta.primaryAction.label}
|
|
38
|
-
icon={ArrowRightIcon}
|
|
39
|
-
variant="primary"
|
|
40
|
-
/>
|
|
41
|
-
) : null}
|
|
42
|
-
{pricingCta.secondaryAction ? (
|
|
43
|
-
<LandingButton
|
|
44
|
-
href={pricingCta.secondaryAction.href}
|
|
45
|
-
label={pricingCta.secondaryAction.label}
|
|
46
|
-
variant="secondary"
|
|
47
|
-
/>
|
|
48
|
-
) : null}
|
|
49
|
-
</div>
|
|
50
|
-
</div>
|
|
51
|
-
</section>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<section className="mx-auto max-w-[1200px] px-6 py-[4.5rem] text-center">
|
|
57
|
-
<h2 className="mb-4 text-[2.5rem] leading-tight font-bold tracking-[-0.03em] text-balance">
|
|
58
|
-
{title}
|
|
59
|
-
</h2>
|
|
60
|
-
<p className="mx-auto mb-[1.875rem] max-w-[520px] text-[1.09375rem] leading-relaxed text-muted-foreground">
|
|
61
|
-
{description}
|
|
62
|
-
</p>
|
|
63
|
-
<div className="flex flex-wrap items-center justify-center gap-3">
|
|
64
|
-
{pricingCta.primaryAction ? (
|
|
65
|
-
<LandingButton
|
|
66
|
-
href={pricingCta.primaryAction.href}
|
|
67
|
-
label={pricingCta.primaryAction.label}
|
|
68
|
-
icon={ArrowRightIcon}
|
|
69
|
-
variant="primary"
|
|
70
|
-
/>
|
|
71
|
-
) : null}
|
|
72
|
-
{pricingCta.secondaryAction ? (
|
|
73
|
-
<LandingButton
|
|
74
|
-
href={pricingCta.secondaryAction.href}
|
|
75
|
-
label={pricingCta.secondaryAction.label}
|
|
76
|
-
variant="secondary"
|
|
77
|
-
/>
|
|
78
|
-
) : null}
|
|
79
|
-
</div>
|
|
80
|
-
</section>
|
|
81
|
-
);
|
|
82
|
-
};
|
|
1
|
+
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
2
|
+
import { ArrowRightIcon } from "lucide-react";
|
|
3
|
+
import { LandingButton } from "./LandingButtons.js";
|
|
4
|
+
|
|
5
|
+
export type PricingCtaSectionProps = {
|
|
6
|
+
pricingCta: NonNullable<ResolvedLandingContent["pricingCta"]>;
|
|
7
|
+
variant?: "default" | "banner";
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const PricingCtaSection = ({
|
|
11
|
+
pricingCta,
|
|
12
|
+
variant = "default",
|
|
13
|
+
}: PricingCtaSectionProps) => {
|
|
14
|
+
const title = pricingCta.title ?? "Start free. Scale when you do.";
|
|
15
|
+
const description =
|
|
16
|
+
pricingCta.description ??
|
|
17
|
+
"Generous free tier. Upgrade only when your app takes off.";
|
|
18
|
+
|
|
19
|
+
if (variant === "banner") {
|
|
20
|
+
return (
|
|
21
|
+
<section className="relative overflow-hidden bg-foreground text-background">
|
|
22
|
+
<div
|
|
23
|
+
aria-hidden
|
|
24
|
+
className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_50%_120%,color-mix(in_oklch,var(--primary)_30%,transparent),transparent_55%)]"
|
|
25
|
+
/>
|
|
26
|
+
<div className="relative mx-auto max-w-[640px] px-6 py-[4.5rem] text-center">
|
|
27
|
+
<h2 className="mb-3.5 text-[2.125rem] leading-tight font-bold tracking-[-0.025em] text-balance">
|
|
28
|
+
{title}
|
|
29
|
+
</h2>
|
|
30
|
+
<p className="mx-auto mb-7 max-w-[520px] text-lg leading-relaxed text-background/70">
|
|
31
|
+
{description}
|
|
32
|
+
</p>
|
|
33
|
+
<div className="flex flex-wrap items-center justify-center gap-3">
|
|
34
|
+
{pricingCta.primaryAction ? (
|
|
35
|
+
<LandingButton
|
|
36
|
+
href={pricingCta.primaryAction.href}
|
|
37
|
+
label={pricingCta.primaryAction.label}
|
|
38
|
+
icon={ArrowRightIcon}
|
|
39
|
+
variant="primary"
|
|
40
|
+
/>
|
|
41
|
+
) : null}
|
|
42
|
+
{pricingCta.secondaryAction ? (
|
|
43
|
+
<LandingButton
|
|
44
|
+
href={pricingCta.secondaryAction.href}
|
|
45
|
+
label={pricingCta.secondaryAction.label}
|
|
46
|
+
variant="secondary"
|
|
47
|
+
/>
|
|
48
|
+
) : null}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</section>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<section className="mx-auto max-w-[1200px] px-6 py-[4.5rem] text-center">
|
|
57
|
+
<h2 className="mb-4 text-[2.5rem] leading-tight font-bold tracking-[-0.03em] text-balance">
|
|
58
|
+
{title}
|
|
59
|
+
</h2>
|
|
60
|
+
<p className="mx-auto mb-[1.875rem] max-w-[520px] text-[1.09375rem] leading-relaxed text-muted-foreground">
|
|
61
|
+
{description}
|
|
62
|
+
</p>
|
|
63
|
+
<div className="flex flex-wrap items-center justify-center gap-3">
|
|
64
|
+
{pricingCta.primaryAction ? (
|
|
65
|
+
<LandingButton
|
|
66
|
+
href={pricingCta.primaryAction.href}
|
|
67
|
+
label={pricingCta.primaryAction.label}
|
|
68
|
+
icon={ArrowRightIcon}
|
|
69
|
+
variant="primary"
|
|
70
|
+
/>
|
|
71
|
+
) : null}
|
|
72
|
+
{pricingCta.secondaryAction ? (
|
|
73
|
+
<LandingButton
|
|
74
|
+
href={pricingCta.secondaryAction.href}
|
|
75
|
+
label={pricingCta.secondaryAction.label}
|
|
76
|
+
variant="secondary"
|
|
77
|
+
/>
|
|
78
|
+
) : null}
|
|
79
|
+
</div>
|
|
80
|
+
</section>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
export type StepsSectionStep = {
|
|
2
|
-
step: string;
|
|
3
|
-
title: string;
|
|
4
|
-
description: string;
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
export type StepsSectionProps = {
|
|
8
|
-
title?: string;
|
|
9
|
-
steps: StepsSectionStep[];
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export const StepsSection = ({
|
|
13
|
-
title = "Three steps to your first request",
|
|
14
|
-
steps,
|
|
15
|
-
}: StepsSectionProps) => {
|
|
16
|
-
if (steps.length === 0) {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<section className="border-y border-border bg-muted/30">
|
|
22
|
-
<div className="mx-auto max-w-[1200px] px-6 py-[3.75rem]">
|
|
23
|
-
<h2 className="mb-10 text-center text-[2.125rem] leading-tight font-bold tracking-[-0.025em]">
|
|
24
|
-
{title}
|
|
25
|
-
</h2>
|
|
26
|
-
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
27
|
-
{steps.map((item) => (
|
|
28
|
-
<div key={item.step}>
|
|
29
|
-
<div className="font-mono text-[2rem] font-bold text-primary">
|
|
30
|
-
{item.step}
|
|
31
|
-
</div>
|
|
32
|
-
<h3 className="mt-2 mb-1.5 text-lg font-semibold">
|
|
33
|
-
{item.title}
|
|
34
|
-
</h3>
|
|
35
|
-
<p className="text-sm leading-relaxed text-muted-foreground">
|
|
36
|
-
{item.description}
|
|
37
|
-
</p>
|
|
38
|
-
</div>
|
|
39
|
-
))}
|
|
40
|
-
</div>
|
|
41
|
-
</div>
|
|
42
|
-
</section>
|
|
43
|
-
);
|
|
44
|
-
};
|
|
1
|
+
export type StepsSectionStep = {
|
|
2
|
+
step: string;
|
|
3
|
+
title: string;
|
|
4
|
+
description: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type StepsSectionProps = {
|
|
8
|
+
title?: string;
|
|
9
|
+
steps: StepsSectionStep[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const StepsSection = ({
|
|
13
|
+
title = "Three steps to your first request",
|
|
14
|
+
steps,
|
|
15
|
+
}: StepsSectionProps) => {
|
|
16
|
+
if (steps.length === 0) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<section className="border-y border-border bg-muted/30">
|
|
22
|
+
<div className="mx-auto max-w-[1200px] px-6 py-[3.75rem]">
|
|
23
|
+
<h2 className="mb-10 text-center text-[2.125rem] leading-tight font-bold tracking-[-0.025em]">
|
|
24
|
+
{title}
|
|
25
|
+
</h2>
|
|
26
|
+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
27
|
+
{steps.map((item) => (
|
|
28
|
+
<div key={item.step}>
|
|
29
|
+
<div className="font-mono text-[2rem] font-bold text-primary">
|
|
30
|
+
{item.step}
|
|
31
|
+
</div>
|
|
32
|
+
<h3 className="mt-2 mb-1.5 text-lg font-semibold">
|
|
33
|
+
{item.title}
|
|
34
|
+
</h3>
|
|
35
|
+
<p className="text-sm leading-relaxed text-muted-foreground">
|
|
36
|
+
{item.description}
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
))}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</section>
|
|
43
|
+
);
|
|
44
|
+
};
|