@lukoweb/apitogo-module-landing 0.1.87 → 0.1.89
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/index.tsx +10 -0
- package/src/sections/ApiShowcaseSection.tsx +85 -30
- package/src/sections/FeaturesSection.tsx +1 -1
- package/src/sections/FooterSection.tsx +13 -2
- package/src/sections/HeroSection.tsx +22 -4
- package/src/sections/PlansPreviewSection.tsx +76 -0
- package/src/sections/PricingCtaSection.tsx +41 -1
- package/src/sections/StepsSection.tsx +42 -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.89",
|
|
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.89",
|
|
18
18
|
"react": "19.2.4",
|
|
19
19
|
"react-dom": "19.2.4",
|
|
20
20
|
"typescript": "5.9.3"
|
package/src/index.tsx
CHANGED
|
@@ -12,8 +12,18 @@ export { TrustSection } from "./sections/TrustSection.js";
|
|
|
12
12
|
export type { TrustSectionProps } from "./sections/TrustSection.js";
|
|
13
13
|
export { FeaturesSection } from "./sections/FeaturesSection.js";
|
|
14
14
|
export type { FeaturesSectionProps } from "./sections/FeaturesSection.js";
|
|
15
|
+
export { StepsSection } from "./sections/StepsSection.js";
|
|
16
|
+
export type {
|
|
17
|
+
StepsSectionProps,
|
|
18
|
+
StepsSectionStep,
|
|
19
|
+
} from "./sections/StepsSection.js";
|
|
15
20
|
export { ApiShowcaseSection } from "./sections/ApiShowcaseSection.js";
|
|
16
21
|
export type { ApiShowcaseSectionProps } from "./sections/ApiShowcaseSection.js";
|
|
22
|
+
export { PlansPreviewSection } from "./sections/PlansPreviewSection.js";
|
|
23
|
+
export type {
|
|
24
|
+
PlansPreviewPlan,
|
|
25
|
+
PlansPreviewSectionProps,
|
|
26
|
+
} from "./sections/PlansPreviewSection.js";
|
|
17
27
|
export { PricingCtaSection } from "./sections/PricingCtaSection.js";
|
|
18
28
|
export type { PricingCtaSectionProps } from "./sections/PricingCtaSection.js";
|
|
19
29
|
export { FooterSection } from "./sections/FooterSection.js";
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
2
|
+
import { cn } from "@lukoweb/apitogo";
|
|
2
3
|
import { Link } from "@lukoweb/apitogo/components";
|
|
3
4
|
import { Button } from "@lukoweb/apitogo/ui/Button.js";
|
|
4
5
|
import { ArrowRightIcon } from "lucide-react";
|
|
5
6
|
import { LandingIcon } from "./LandingIcon.js";
|
|
6
7
|
|
|
7
8
|
export type ApiShowcaseSectionProps = {
|
|
8
|
-
apiShowcase: NonNullable<ResolvedLandingContent["apiShowcase"]
|
|
9
|
+
apiShowcase: NonNullable<ResolvedLandingContent["apiShowcase"]> & {
|
|
10
|
+
layout?: "grid" | "list";
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const methodTone = (method?: string) => {
|
|
15
|
+
const m = (method ?? "").toUpperCase();
|
|
16
|
+
if (m === "POST" || m === "PUT" || m === "PATCH") {
|
|
17
|
+
return "bg-emerald-50 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300";
|
|
18
|
+
}
|
|
19
|
+
if (m === "DELETE") {
|
|
20
|
+
return "bg-red-50 text-red-600 dark:bg-red-500/15 dark:text-red-300";
|
|
21
|
+
}
|
|
22
|
+
return "bg-blue-50 text-blue-700 dark:bg-blue-500/15 dark:text-blue-300";
|
|
9
23
|
};
|
|
10
24
|
|
|
11
25
|
export const ApiShowcaseSection = ({
|
|
@@ -18,15 +32,18 @@ export const ApiShowcaseSection = ({
|
|
|
18
32
|
|
|
19
33
|
const browseHref = apiShowcase.browseAllHref ?? "/api";
|
|
20
34
|
const browseLabel = apiShowcase.browseAllLabel ?? "Browse all APIs";
|
|
35
|
+
const layout = apiShowcase.layout ?? "grid";
|
|
21
36
|
|
|
22
37
|
return (
|
|
23
|
-
<section className="border-y border-border bg-
|
|
38
|
+
<section className="border-y border-border bg-background">
|
|
24
39
|
<div className="mx-auto max-w-[1200px] px-6 py-[3.75rem]">
|
|
25
40
|
<div className="mb-8 flex flex-wrap items-end justify-between gap-5">
|
|
26
41
|
<div>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
42
|
+
{layout === "grid" ? (
|
|
43
|
+
<p className="mb-3 font-mono text-[0.8125rem] font-semibold text-primary">
|
|
44
|
+
{"// the catalog"}
|
|
45
|
+
</p>
|
|
46
|
+
) : null}
|
|
30
47
|
<h2 className="text-[2.125rem] leading-tight font-bold tracking-[-0.025em]">
|
|
31
48
|
{apiShowcase.title ?? "Production APIs. One key."}
|
|
32
49
|
</h2>
|
|
@@ -44,32 +61,70 @@ export const ApiShowcaseSection = ({
|
|
|
44
61
|
</Button>
|
|
45
62
|
</div>
|
|
46
63
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
{layout === "list" ? (
|
|
65
|
+
<div className="overflow-hidden rounded-[14px] border border-border bg-card">
|
|
66
|
+
{items.map((item, index) => {
|
|
67
|
+
const method =
|
|
68
|
+
"method" in item && typeof item.method === "string"
|
|
69
|
+
? item.method
|
|
70
|
+
: undefined;
|
|
71
|
+
const description = item.summary ?? item.name;
|
|
72
|
+
return (
|
|
73
|
+
<div
|
|
74
|
+
key={`${item.name}-${item.path}`}
|
|
75
|
+
className={cn(
|
|
76
|
+
"flex flex-wrap items-center gap-3.5 px-5 py-3.5",
|
|
77
|
+
index < items.length - 1 && "border-b border-border",
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
{method ? (
|
|
81
|
+
<span
|
|
82
|
+
className={cn(
|
|
83
|
+
"min-w-14 rounded-[5px] px-2 py-0.5 text-center font-mono text-[11px] font-bold",
|
|
84
|
+
methodTone(method),
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
{method.toUpperCase()}
|
|
88
|
+
</span>
|
|
89
|
+
) : null}
|
|
90
|
+
<code className="flex-1 font-mono text-[0.84375rem] text-foreground">
|
|
91
|
+
{item.path}
|
|
92
|
+
</code>
|
|
93
|
+
<span className="text-[0.8125rem] text-muted-foreground">
|
|
94
|
+
{description}
|
|
61
95
|
</span>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
})}
|
|
99
|
+
</div>
|
|
100
|
+
) : (
|
|
101
|
+
<div className="grid gap-3.5 sm:grid-cols-2 lg:grid-cols-4">
|
|
102
|
+
{items.map((item) => (
|
|
103
|
+
<Link
|
|
104
|
+
key={`${item.name}-${item.path}`}
|
|
105
|
+
to={browseHref}
|
|
106
|
+
className="block rounded-[13px] border border-border bg-card p-[1.125rem] text-foreground no-underline transition-[border-color,transform] hover:-translate-y-0.5 hover:border-muted-foreground/30"
|
|
107
|
+
>
|
|
108
|
+
<div className="mb-3.5 flex items-center justify-between">
|
|
109
|
+
<div className="grid size-9 place-items-center rounded-[9px] border border-border bg-muted/40 text-primary">
|
|
110
|
+
<LandingIcon name={item.icon ?? "boxes"} size={18} />
|
|
111
|
+
</div>
|
|
112
|
+
{item.latency ? (
|
|
113
|
+
<span className="rounded-md bg-primary/10 px-[0.4375rem] py-[0.1875rem] font-mono text-[0.6875rem] font-semibold text-primary">
|
|
114
|
+
{item.latency}
|
|
115
|
+
</span>
|
|
116
|
+
) : null}
|
|
117
|
+
</div>
|
|
118
|
+
<h3 className="mb-1 text-[0.9375rem] font-semibold">
|
|
119
|
+
{item.name}
|
|
120
|
+
</h3>
|
|
121
|
+
<p className="font-mono text-[0.71875rem] text-muted-foreground/70">
|
|
122
|
+
{item.path}
|
|
123
|
+
</p>
|
|
124
|
+
</Link>
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
73
128
|
</div>
|
|
74
129
|
</section>
|
|
75
130
|
);
|
|
@@ -21,7 +21,7 @@ export const FeaturesSection = ({
|
|
|
21
21
|
"Skip the infra. Focus on your product while your developer portal handles docs, auth, and billing.";
|
|
22
22
|
|
|
23
23
|
return (
|
|
24
|
-
<section className="mx-auto max-w-[1200px] px-6 py-10 pb-16">
|
|
24
|
+
<section id="features" className="mx-auto max-w-[1200px] px-6 py-10 pb-16">
|
|
25
25
|
<div className="mb-11 max-w-[640px]">
|
|
26
26
|
<p className="mb-3 font-mono text-[0.8125rem] font-semibold text-primary">
|
|
27
27
|
{label}
|
|
@@ -98,8 +98,19 @@ export const FooterSection = ({ footer }: FooterSectionProps) => {
|
|
|
98
98
|
/>
|
|
99
99
|
) : (
|
|
100
100
|
<span className="text-[1.125rem] font-bold tracking-[-0.02em]">
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
{brandName.includes("API") && brandName !== "API ToGo" ? (
|
|
102
|
+
<>
|
|
103
|
+
{brandName.replace(/API$/, "")}
|
|
104
|
+
<span className="text-primary">API</span>
|
|
105
|
+
</>
|
|
106
|
+
) : footer.brand?.name ? (
|
|
107
|
+
brandName
|
|
108
|
+
) : (
|
|
109
|
+
<>
|
|
110
|
+
<span className="text-primary">API</span>
|
|
111
|
+
<span>ToGo</span>
|
|
112
|
+
</>
|
|
113
|
+
)}
|
|
103
114
|
</span>
|
|
104
115
|
)}
|
|
105
116
|
</div>
|
|
@@ -4,18 +4,31 @@ import { ArrowRightIcon, BookOpenIcon, CheckIcon } from "lucide-react";
|
|
|
4
4
|
import { CodeExampleCard } from "./CodeExampleCard.js";
|
|
5
5
|
import { LandingButton } from "./LandingButtons.js";
|
|
6
6
|
|
|
7
|
+
type CodeExample = NonNullable<
|
|
8
|
+
NonNullable<ResolvedLandingContent["hero"]>["codeExample"]
|
|
9
|
+
>;
|
|
10
|
+
|
|
7
11
|
export type HeroSectionProps = {
|
|
8
12
|
hero: NonNullable<ResolvedLandingContent["hero"]>;
|
|
9
13
|
primaryAction?: ResolvedLandingContent["primaryAction"];
|
|
10
14
|
secondaryAction?: ResolvedLandingContent["secondaryAction"];
|
|
15
|
+
/** Optional stacked code cards (falls back to hero.codeExample). */
|
|
16
|
+
codeExamples?: CodeExample[];
|
|
11
17
|
};
|
|
12
18
|
|
|
13
19
|
export const HeroSection = ({
|
|
14
20
|
hero,
|
|
15
21
|
primaryAction,
|
|
16
22
|
secondaryAction,
|
|
23
|
+
codeExamples,
|
|
17
24
|
}: HeroSectionProps) => {
|
|
18
|
-
const
|
|
25
|
+
const examples =
|
|
26
|
+
codeExamples && codeExamples.length > 0
|
|
27
|
+
? codeExamples.filter((example) => Boolean(example.code))
|
|
28
|
+
: hero.codeExample?.code
|
|
29
|
+
? [hero.codeExample]
|
|
30
|
+
: [];
|
|
31
|
+
const hasCodeExample = examples.length > 0;
|
|
19
32
|
const highlights = hero.highlights ?? [];
|
|
20
33
|
|
|
21
34
|
return (
|
|
@@ -106,9 +119,14 @@ export const HeroSection = ({
|
|
|
106
119
|
) : null}
|
|
107
120
|
</div>
|
|
108
121
|
|
|
109
|
-
{hasCodeExample
|
|
110
|
-
<div className="landing-fu">
|
|
111
|
-
|
|
122
|
+
{hasCodeExample ? (
|
|
123
|
+
<div className="landing-fu flex flex-col gap-3.5">
|
|
124
|
+
{examples.map((example) => (
|
|
125
|
+
<CodeExampleCard
|
|
126
|
+
key={`${example.filename ?? "example"}-${example.code.slice(0, 24)}`}
|
|
127
|
+
example={example}
|
|
128
|
+
/>
|
|
129
|
+
))}
|
|
112
130
|
</div>
|
|
113
131
|
) : null}
|
|
114
132
|
</div>
|
|
@@ -0,0 +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
|
+
};
|
|
@@ -4,14 +4,54 @@ import { LandingButton } from "./LandingButtons.js";
|
|
|
4
4
|
|
|
5
5
|
export type PricingCtaSectionProps = {
|
|
6
6
|
pricingCta: NonNullable<ResolvedLandingContent["pricingCta"]>;
|
|
7
|
+
variant?: "default" | "banner";
|
|
7
8
|
};
|
|
8
9
|
|
|
9
|
-
export const PricingCtaSection = ({
|
|
10
|
+
export const PricingCtaSection = ({
|
|
11
|
+
pricingCta,
|
|
12
|
+
variant = "default",
|
|
13
|
+
}: PricingCtaSectionProps) => {
|
|
10
14
|
const title = pricingCta.title ?? "Start free. Scale when you do.";
|
|
11
15
|
const description =
|
|
12
16
|
pricingCta.description ??
|
|
13
17
|
"Generous free tier. Upgrade only when your app takes off.";
|
|
14
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
|
+
|
|
15
55
|
return (
|
|
16
56
|
<section className="mx-auto max-w-[1200px] px-6 py-[4.5rem] text-center">
|
|
17
57
|
<h2 className="mb-4 text-[2.5rem] leading-tight font-bold tracking-[-0.03em] text-balance">
|
|
@@ -0,0 +1,42 @@
|
|
|
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">{item.title}</h3>
|
|
33
|
+
<p className="text-sm leading-relaxed text-muted-foreground">
|
|
34
|
+
{item.description}
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</section>
|
|
41
|
+
);
|
|
42
|
+
};
|