@lukoweb/apitogo-module-landing 0.1.53 → 0.1.55
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 +29 -69
- package/src/sections/ApiShowcaseSection.tsx +74 -0
- package/src/sections/CodeExampleCard.tsx +28 -0
- package/src/sections/FeaturesSection.tsx +49 -0
- package/src/sections/HeroSection.tsx +99 -0
- package/src/sections/LandingIcon.tsx +50 -0
- package/src/sections/PricingCtaSection.tsx +43 -0
- package/src/sections/TickerBar.tsx +44 -0
- package/src/sections/TrustSection.tsx +32 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lukoweb/apitogo-module-landing",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.55",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.tsx"
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@types/react": "19.2.14",
|
|
16
16
|
"@types/react-dom": "19.2.3",
|
|
17
|
-
"@lukoweb/apitogo": "0.1.
|
|
17
|
+
"@lukoweb/apitogo": "0.1.55",
|
|
18
18
|
"react": "19.2.4",
|
|
19
19
|
"react-dom": "19.2.4",
|
|
20
20
|
"typescript": "5.9.3"
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { cn } from "@lukoweb/apitogo";
|
|
2
2
|
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
3
|
-
import { Head,
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { Head, useZudoku } from "@lukoweb/apitogo/components";
|
|
4
|
+
import { ApiShowcaseSection } from "./sections/ApiShowcaseSection.js";
|
|
5
|
+
import { FeaturesSection } from "./sections/FeaturesSection.js";
|
|
6
|
+
import { HeroSection } from "./sections/HeroSection.js";
|
|
7
|
+
import { PricingCtaSection } from "./sections/PricingCtaSection.js";
|
|
8
|
+
import { TickerBar } from "./sections/TickerBar.js";
|
|
9
|
+
import { TrustSection } from "./sections/TrustSection.js";
|
|
6
10
|
|
|
7
11
|
export type DefaultLandingPageProps = {
|
|
8
12
|
content: ResolvedLandingContent;
|
|
@@ -13,18 +17,19 @@ export const DefaultLandingPage = ({ content }: DefaultLandingPageProps) => {
|
|
|
13
17
|
const siteTitle =
|
|
14
18
|
options.metadata?.applicationName ?? options.site?.title ?? "APIToGo";
|
|
15
19
|
|
|
16
|
-
const hero = content.hero
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
const hero = content.hero ?? {
|
|
21
|
+
title: siteTitle,
|
|
22
|
+
description:
|
|
23
|
+
options.metadata?.description ??
|
|
24
|
+
"Beautiful API documentation and developer portals powered by APIToGo.",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const title = hero.title ?? siteTitle;
|
|
19
28
|
const description =
|
|
20
|
-
hero
|
|
29
|
+
hero.description ??
|
|
21
30
|
options.metadata?.description ??
|
|
22
31
|
"Beautiful API documentation and developer portals powered by APIToGo.";
|
|
23
32
|
|
|
24
|
-
const features = content.features ?? [];
|
|
25
|
-
const primaryAction = content.primaryAction;
|
|
26
|
-
const secondaryAction = content.secondaryAction;
|
|
27
|
-
|
|
28
33
|
return (
|
|
29
34
|
<div className="relative overflow-hidden">
|
|
30
35
|
<Head>
|
|
@@ -32,66 +37,21 @@ export const DefaultLandingPage = ({ content }: DefaultLandingPageProps) => {
|
|
|
32
37
|
{description ? <meta name="description" content={description} /> : null}
|
|
33
38
|
</Head>
|
|
34
39
|
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
<HeroSection
|
|
41
|
+
hero={{ ...hero, title, description }}
|
|
42
|
+
primaryAction={content.primaryAction}
|
|
43
|
+
secondaryAction={content.secondaryAction}
|
|
38
44
|
/>
|
|
39
45
|
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
</h1>
|
|
50
|
-
<p className="max-w-2xl text-lg text-muted-foreground text-pretty">
|
|
51
|
-
{description}
|
|
52
|
-
</p>
|
|
53
|
-
{(primaryAction || secondaryAction) && (
|
|
54
|
-
<div className="flex flex-wrap items-center justify-center gap-3 pt-2">
|
|
55
|
-
{primaryAction ? (
|
|
56
|
-
<Button asChild size="lg">
|
|
57
|
-
<Link to={primaryAction.href}>
|
|
58
|
-
{primaryAction.label}
|
|
59
|
-
<ArrowRightIcon className="ms-1" />
|
|
60
|
-
</Link>
|
|
61
|
-
</Button>
|
|
62
|
-
) : null}
|
|
63
|
-
{secondaryAction ? (
|
|
64
|
-
<Button asChild variant="outline" size="lg">
|
|
65
|
-
<Link to={secondaryAction.href}>{secondaryAction.label}</Link>
|
|
66
|
-
</Button>
|
|
67
|
-
) : null}
|
|
68
|
-
</div>
|
|
69
|
-
)}
|
|
70
|
-
</div>
|
|
71
|
-
|
|
72
|
-
{features.length > 0 ? (
|
|
73
|
-
<div className="grid gap-4 pt-8 sm:grid-cols-2 lg:grid-cols-3">
|
|
74
|
-
{features.map((feature) => (
|
|
75
|
-
<article
|
|
76
|
-
key={feature.title}
|
|
77
|
-
className="rounded-xl border bg-card/60 p-6 shadow-xs backdrop-blur-sm"
|
|
78
|
-
>
|
|
79
|
-
<div className="mb-4 flex size-10 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
80
|
-
{feature.title.toLowerCase().includes("api") ? (
|
|
81
|
-
<CodeIcon size={20} />
|
|
82
|
-
) : (
|
|
83
|
-
<BookOpenIcon size={20} />
|
|
84
|
-
)}
|
|
85
|
-
</div>
|
|
86
|
-
<h2 className="text-lg font-medium">{feature.title}</h2>
|
|
87
|
-
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
|
88
|
-
{feature.description}
|
|
89
|
-
</p>
|
|
90
|
-
</article>
|
|
91
|
-
))}
|
|
92
|
-
</div>
|
|
93
|
-
) : null}
|
|
94
|
-
</section>
|
|
46
|
+
{content.ticker ? <TickerBar ticker={content.ticker} /> : null}
|
|
47
|
+
{content.trust ? <TrustSection trust={content.trust} /> : null}
|
|
48
|
+
{content.features ? <FeaturesSection features={content.features} /> : null}
|
|
49
|
+
{content.apiShowcase ? (
|
|
50
|
+
<ApiShowcaseSection apiShowcase={content.apiShowcase} />
|
|
51
|
+
) : null}
|
|
52
|
+
{content.pricingCta ? (
|
|
53
|
+
<PricingCtaSection pricingCta={content.pricingCta} />
|
|
54
|
+
) : null}
|
|
95
55
|
|
|
96
56
|
{content.showPoweredBy !== false ? (
|
|
97
57
|
<div
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
2
|
+
import { Link } from "@lukoweb/apitogo/components";
|
|
3
|
+
import { ArrowRightIcon } from "lucide-react";
|
|
4
|
+
import { LandingIcon } from "./LandingIcon.js";
|
|
5
|
+
|
|
6
|
+
export type ApiShowcaseSectionProps = {
|
|
7
|
+
apiShowcase: NonNullable<ResolvedLandingContent["apiShowcase"]>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const ApiShowcaseSection = ({
|
|
11
|
+
apiShowcase,
|
|
12
|
+
}: ApiShowcaseSectionProps) => {
|
|
13
|
+
const items = apiShowcase.items ?? [];
|
|
14
|
+
if (items.length === 0) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const browseHref = apiShowcase.browseAllHref ?? "/api";
|
|
19
|
+
const browseLabel = apiShowcase.browseAllLabel ?? "Browse all APIs";
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<section className="border-y bg-muted/20">
|
|
23
|
+
<div className="mx-auto max-w-screen-xl px-4 py-14 lg:px-8">
|
|
24
|
+
<div className="mb-8 flex flex-wrap items-end justify-between gap-4">
|
|
25
|
+
<div>
|
|
26
|
+
<p className="mb-3 font-mono text-sm font-semibold text-primary">
|
|
27
|
+
// the catalog
|
|
28
|
+
</p>
|
|
29
|
+
<h2 className="text-3xl font-semibold tracking-tight">
|
|
30
|
+
{apiShowcase.title ?? "Production APIs. One key."}
|
|
31
|
+
</h2>
|
|
32
|
+
{apiShowcase.subtitle ? (
|
|
33
|
+
<p className="mt-2 text-muted-foreground">
|
|
34
|
+
{apiShowcase.subtitle}
|
|
35
|
+
</p>
|
|
36
|
+
) : null}
|
|
37
|
+
</div>
|
|
38
|
+
<Link
|
|
39
|
+
to={browseHref}
|
|
40
|
+
className="inline-flex items-center gap-2 rounded-lg border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted/50"
|
|
41
|
+
>
|
|
42
|
+
{browseLabel}
|
|
43
|
+
<ArrowRightIcon size={15} />
|
|
44
|
+
</Link>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
48
|
+
{items.map((item) => (
|
|
49
|
+
<Link
|
|
50
|
+
key={`${item.name}-${item.path}`}
|
|
51
|
+
to={browseHref}
|
|
52
|
+
className="rounded-xl border bg-card/70 p-4 transition hover:-translate-y-0.5 hover:border-primary/30"
|
|
53
|
+
>
|
|
54
|
+
<div className="mb-3 flex items-center justify-between">
|
|
55
|
+
<div className="flex size-9 items-center justify-center rounded-lg border bg-muted/40 text-primary">
|
|
56
|
+
<LandingIcon name={item.icon ?? "boxes"} size={18} />
|
|
57
|
+
</div>
|
|
58
|
+
{item.latency ? (
|
|
59
|
+
<span className="rounded-md bg-primary/10 px-2 py-0.5 font-mono text-[11px] font-semibold text-primary">
|
|
60
|
+
{item.latency}
|
|
61
|
+
</span>
|
|
62
|
+
) : null}
|
|
63
|
+
</div>
|
|
64
|
+
<h3 className="font-medium">{item.name}</h3>
|
|
65
|
+
<p className="mt-1 font-mono text-[11px] text-muted-foreground">
|
|
66
|
+
{item.path}
|
|
67
|
+
</p>
|
|
68
|
+
</Link>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</section>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
2
|
+
import { TerminalIcon } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
export type CodeExampleCardProps = {
|
|
5
|
+
example: NonNullable<
|
|
6
|
+
NonNullable<ResolvedLandingContent["hero"]>["codeExample"]
|
|
7
|
+
>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const CodeExampleCard = ({ example }: CodeExampleCardProps) => {
|
|
11
|
+
const filename = example.filename ?? "example.sh";
|
|
12
|
+
const language = example.language ?? "bash";
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="overflow-hidden rounded-2xl border bg-[#0c0f16] shadow-lg">
|
|
16
|
+
<div className="flex items-center gap-2 border-b border-white/10 px-4 py-3">
|
|
17
|
+
<span className="size-2.5 rounded-full bg-[#ff5f57]" />
|
|
18
|
+
<span className="size-2.5 rounded-full bg-[#febc2e]" />
|
|
19
|
+
<span className="size-2.5 rounded-full bg-[#28c840]" />
|
|
20
|
+
<span className="ms-2 font-mono text-xs text-white/45">{filename}</span>
|
|
21
|
+
<TerminalIcon size={14} className="ms-auto text-white/30" />
|
|
22
|
+
</div>
|
|
23
|
+
<pre className="overflow-x-auto p-4 font-mono text-xs leading-relaxed text-[#cdd3e0]">
|
|
24
|
+
<code data-language={language}>{example.code}</code>
|
|
25
|
+
</pre>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
2
|
+
import { LandingIcon } from "./LandingIcon.js";
|
|
3
|
+
|
|
4
|
+
export type FeaturesSectionProps = {
|
|
5
|
+
features: NonNullable<ResolvedLandingContent["features"]>;
|
|
6
|
+
title?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const FeaturesSection = ({
|
|
11
|
+
features,
|
|
12
|
+
title = "Everything you need to build on your API",
|
|
13
|
+
description = "Skip the infra. Focus on your product while your developer portal handles docs, auth, and billing.",
|
|
14
|
+
}: FeaturesSectionProps) => {
|
|
15
|
+
if (features.length === 0) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<section className="mx-auto max-w-screen-xl px-4 py-12 lg:px-8 lg:pb-16">
|
|
21
|
+
<div className="mb-10 max-w-2xl">
|
|
22
|
+
<p className="mb-3 font-mono text-sm font-semibold text-primary">
|
|
23
|
+
// why us
|
|
24
|
+
</p>
|
|
25
|
+
<h2 className="text-3xl font-semibold tracking-tight text-balance">
|
|
26
|
+
{title}
|
|
27
|
+
</h2>
|
|
28
|
+
<p className="mt-3 text-lg text-muted-foreground">{description}</p>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
32
|
+
{features.map((feature) => (
|
|
33
|
+
<article
|
|
34
|
+
key={feature.title}
|
|
35
|
+
className="rounded-xl border bg-card/60 p-6 shadow-xs backdrop-blur-sm"
|
|
36
|
+
>
|
|
37
|
+
<div className="mb-4 flex size-10 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
38
|
+
<LandingIcon name={feature.icon} />
|
|
39
|
+
</div>
|
|
40
|
+
<h3 className="text-lg font-medium">{feature.title}</h3>
|
|
41
|
+
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
|
42
|
+
{feature.description}
|
|
43
|
+
</p>
|
|
44
|
+
</article>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
</section>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
2
|
+
import { Link } from "@lukoweb/apitogo/components";
|
|
3
|
+
import { Button } from "@lukoweb/apitogo/ui/Button.js";
|
|
4
|
+
import { ArrowRightIcon, CheckIcon } from "lucide-react";
|
|
5
|
+
import { CodeExampleCard } from "./CodeExampleCard.js";
|
|
6
|
+
|
|
7
|
+
export type HeroSectionProps = {
|
|
8
|
+
hero: NonNullable<ResolvedLandingContent["hero"]>;
|
|
9
|
+
primaryAction?: ResolvedLandingContent["primaryAction"];
|
|
10
|
+
secondaryAction?: ResolvedLandingContent["secondaryAction"];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const HeroSection = ({
|
|
14
|
+
hero,
|
|
15
|
+
primaryAction,
|
|
16
|
+
secondaryAction,
|
|
17
|
+
}: HeroSectionProps) => {
|
|
18
|
+
const hasCodeExample = Boolean(hero.codeExample?.code);
|
|
19
|
+
const highlights = hero.highlights ?? [];
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<section className="relative overflow-hidden border-b">
|
|
23
|
+
<div
|
|
24
|
+
aria-hidden
|
|
25
|
+
className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[520px] bg-[radial-gradient(ellipse_at_top_right,var(--primary)_0%,transparent_70%)] opacity-[0.12]"
|
|
26
|
+
/>
|
|
27
|
+
|
|
28
|
+
<div
|
|
29
|
+
className={
|
|
30
|
+
hasCodeExample
|
|
31
|
+
? "mx-auto grid max-w-screen-xl items-center gap-10 px-4 py-16 lg:grid-cols-2 lg:gap-14 lg:px-8 lg:py-20"
|
|
32
|
+
: "mx-auto flex max-w-screen-xl flex-col gap-10 px-4 py-16 lg:px-8 lg:py-24"
|
|
33
|
+
}
|
|
34
|
+
>
|
|
35
|
+
<div
|
|
36
|
+
className={
|
|
37
|
+
hasCodeExample
|
|
38
|
+
? "flex flex-col gap-6"
|
|
39
|
+
: "mx-auto flex max-w-3xl flex-col items-center gap-6 text-center"
|
|
40
|
+
}
|
|
41
|
+
>
|
|
42
|
+
{(hero.badge || hero.subtitle) && (
|
|
43
|
+
<div className="inline-flex w-fit flex-wrap items-center gap-2 rounded-full border bg-muted/50 px-3 py-1 text-xs font-medium text-muted-foreground">
|
|
44
|
+
{hero.badge ? (
|
|
45
|
+
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary/10 px-2 py-0.5 font-semibold text-primary">
|
|
46
|
+
{hero.badge}
|
|
47
|
+
</span>
|
|
48
|
+
) : null}
|
|
49
|
+
{hero.subtitle ? <span>{hero.subtitle}</span> : null}
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
|
|
53
|
+
<h1 className="text-4xl font-semibold tracking-tight text-balance sm:text-5xl lg:text-[3.25rem] lg:leading-[1.05]">
|
|
54
|
+
{hero.title}
|
|
55
|
+
</h1>
|
|
56
|
+
|
|
57
|
+
{hero.description ? (
|
|
58
|
+
<p className="max-w-xl text-lg text-muted-foreground text-pretty">
|
|
59
|
+
{hero.description}
|
|
60
|
+
</p>
|
|
61
|
+
) : null}
|
|
62
|
+
|
|
63
|
+
{(primaryAction || secondaryAction) && (
|
|
64
|
+
<div className="flex flex-wrap items-center gap-3 pt-1">
|
|
65
|
+
{primaryAction ? (
|
|
66
|
+
<Button asChild size="lg">
|
|
67
|
+
<Link to={primaryAction.href}>
|
|
68
|
+
{primaryAction.label}
|
|
69
|
+
<ArrowRightIcon className="ms-1" />
|
|
70
|
+
</Link>
|
|
71
|
+
</Button>
|
|
72
|
+
) : null}
|
|
73
|
+
{secondaryAction ? (
|
|
74
|
+
<Button asChild variant="outline" size="lg">
|
|
75
|
+
<Link to={secondaryAction.href}>{secondaryAction.label}</Link>
|
|
76
|
+
</Button>
|
|
77
|
+
) : null}
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
|
|
81
|
+
{highlights.length > 0 ? (
|
|
82
|
+
<ul className="flex flex-wrap items-center gap-5 pt-2 text-sm text-muted-foreground">
|
|
83
|
+
{highlights.map((item) => (
|
|
84
|
+
<li key={item} className="inline-flex items-center gap-2">
|
|
85
|
+
<CheckIcon size={15} className="text-primary" />
|
|
86
|
+
{item}
|
|
87
|
+
</li>
|
|
88
|
+
))}
|
|
89
|
+
</ul>
|
|
90
|
+
) : null}
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{hasCodeExample && hero.codeExample ? (
|
|
94
|
+
<CodeExampleCard example={hero.codeExample} />
|
|
95
|
+
) : null}
|
|
96
|
+
</div>
|
|
97
|
+
</section>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { LucideIcon } from "lucide-react";
|
|
2
|
+
import {
|
|
3
|
+
ActivityIcon,
|
|
4
|
+
ArrowRightLeftIcon,
|
|
5
|
+
BadgeDollarSignIcon,
|
|
6
|
+
BookOpenIcon,
|
|
7
|
+
BoxesIcon,
|
|
8
|
+
CheckIcon,
|
|
9
|
+
CodeIcon,
|
|
10
|
+
CoinsIcon,
|
|
11
|
+
GaugeIcon,
|
|
12
|
+
LayersIcon,
|
|
13
|
+
RadioIcon,
|
|
14
|
+
ShieldCheckIcon,
|
|
15
|
+
WebhookIcon,
|
|
16
|
+
ZapIcon,
|
|
17
|
+
} from "lucide-react";
|
|
18
|
+
|
|
19
|
+
const ICONS: Record<string, LucideIcon> = {
|
|
20
|
+
activity: ActivityIcon,
|
|
21
|
+
"arrow-left-right": ArrowRightLeftIcon,
|
|
22
|
+
"arrow-right-left": ArrowRightLeftIcon,
|
|
23
|
+
"badge-dollar-sign": BadgeDollarSignIcon,
|
|
24
|
+
"book-open": BookOpenIcon,
|
|
25
|
+
boxes: BoxesIcon,
|
|
26
|
+
check: CheckIcon,
|
|
27
|
+
code: CodeIcon,
|
|
28
|
+
coins: CoinsIcon,
|
|
29
|
+
gauge: GaugeIcon,
|
|
30
|
+
layers: LayersIcon,
|
|
31
|
+
radio: RadioIcon,
|
|
32
|
+
"shield-check": ShieldCheckIcon,
|
|
33
|
+
webhook: WebhookIcon,
|
|
34
|
+
zap: ZapIcon,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type LandingIconProps = {
|
|
38
|
+
name?: string;
|
|
39
|
+
size?: number;
|
|
40
|
+
className?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const LandingIcon = ({
|
|
44
|
+
name,
|
|
45
|
+
size = 20,
|
|
46
|
+
className,
|
|
47
|
+
}: LandingIconProps) => {
|
|
48
|
+
const Icon = (name && ICONS[name]) || CodeIcon;
|
|
49
|
+
return <Icon size={size} className={className} aria-hidden />;
|
|
50
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
2
|
+
import { Link } from "@lukoweb/apitogo/components";
|
|
3
|
+
import { Button } from "@lukoweb/apitogo/ui/Button.js";
|
|
4
|
+
import { ArrowRightIcon } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
export type PricingCtaSectionProps = {
|
|
7
|
+
pricingCta: NonNullable<ResolvedLandingContent["pricingCta"]>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const PricingCtaSection = ({ pricingCta }: PricingCtaSectionProps) => {
|
|
11
|
+
const title = pricingCta.title ?? "Start free. Scale when you do.";
|
|
12
|
+
const description =
|
|
13
|
+
pricingCta.description ??
|
|
14
|
+
"Generous free tier. Upgrade only when your app takes off.";
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<section className="mx-auto max-w-screen-xl px-4 py-16 text-center lg:px-8 lg:py-20">
|
|
18
|
+
<h2 className="text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
|
|
19
|
+
{title}
|
|
20
|
+
</h2>
|
|
21
|
+
<p className="mx-auto mt-4 max-w-xl text-lg text-muted-foreground">
|
|
22
|
+
{description}
|
|
23
|
+
</p>
|
|
24
|
+
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
|
|
25
|
+
{pricingCta.primaryAction ? (
|
|
26
|
+
<Button asChild size="lg">
|
|
27
|
+
<Link to={pricingCta.primaryAction.href}>
|
|
28
|
+
{pricingCta.primaryAction.label}
|
|
29
|
+
<ArrowRightIcon className="ms-1" />
|
|
30
|
+
</Link>
|
|
31
|
+
</Button>
|
|
32
|
+
) : null}
|
|
33
|
+
{pricingCta.secondaryAction ? (
|
|
34
|
+
<Button asChild variant="outline" size="lg">
|
|
35
|
+
<Link to={pricingCta.secondaryAction.href}>
|
|
36
|
+
{pricingCta.secondaryAction.label}
|
|
37
|
+
</Link>
|
|
38
|
+
</Button>
|
|
39
|
+
) : null}
|
|
40
|
+
</div>
|
|
41
|
+
</section>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
2
|
+
|
|
3
|
+
export type TickerBarProps = {
|
|
4
|
+
ticker: NonNullable<ResolvedLandingContent["ticker"]>;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const TickerBar = ({ ticker }: TickerBarProps) => {
|
|
8
|
+
const items = ticker.items ?? [];
|
|
9
|
+
if (items.length === 0) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const loop = [...items, ...items];
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="overflow-hidden border-t bg-muted/30">
|
|
17
|
+
<div className="flex w-max animate-[ticker_34s_linear_infinite]">
|
|
18
|
+
{loop.map((item, index) => (
|
|
19
|
+
<div
|
|
20
|
+
key={`${item.label}-${index}`}
|
|
21
|
+
className="flex items-center gap-2.5 whitespace-nowrap border-r px-6 py-3 font-mono text-sm"
|
|
22
|
+
>
|
|
23
|
+
<span className="font-semibold text-muted-foreground">
|
|
24
|
+
{item.label}
|
|
25
|
+
</span>
|
|
26
|
+
<span className="font-semibold">{item.value}</span>
|
|
27
|
+
{item.change ? (
|
|
28
|
+
<span
|
|
29
|
+
className={
|
|
30
|
+
item.change.trim().startsWith("-")
|
|
31
|
+
? "font-semibold text-destructive"
|
|
32
|
+
: "font-semibold text-primary"
|
|
33
|
+
}
|
|
34
|
+
>
|
|
35
|
+
{item.change}
|
|
36
|
+
</span>
|
|
37
|
+
) : null}
|
|
38
|
+
</div>
|
|
39
|
+
))}
|
|
40
|
+
</div>
|
|
41
|
+
<style>{`@keyframes ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } }`}</style>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
2
|
+
|
|
3
|
+
export type TrustSectionProps = {
|
|
4
|
+
trust: NonNullable<ResolvedLandingContent["trust"]>;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const TrustSection = ({ trust }: TrustSectionProps) => {
|
|
8
|
+
const logos = trust.logos ?? [];
|
|
9
|
+
if (logos.length === 0) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<section className="mx-auto max-w-screen-xl px-4 py-10 lg:px-8">
|
|
15
|
+
{trust.title ? (
|
|
16
|
+
<p className="mb-6 text-center text-xs font-semibold tracking-[0.08em] text-muted-foreground uppercase">
|
|
17
|
+
{trust.title}
|
|
18
|
+
</p>
|
|
19
|
+
) : null}
|
|
20
|
+
<div className="flex flex-wrap items-center justify-center gap-x-10 gap-y-4 opacity-80">
|
|
21
|
+
{logos.map((logo) => (
|
|
22
|
+
<span
|
|
23
|
+
key={logo}
|
|
24
|
+
className="text-lg font-semibold tracking-tight text-muted-foreground"
|
|
25
|
+
>
|
|
26
|
+
{logo}
|
|
27
|
+
</span>
|
|
28
|
+
))}
|
|
29
|
+
</div>
|
|
30
|
+
</section>
|
|
31
|
+
);
|
|
32
|
+
};
|