@lukoweb/apitogo-module-landing 0.2.22 → 0.2.24
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
package/src/landing.css
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
@keyframes landing-fu {
|
|
2
|
-
from {
|
|
3
|
-
opacity: 0;
|
|
4
|
-
transform: translateY(10px);
|
|
5
|
-
}
|
|
6
|
-
to {
|
|
7
|
-
opacity: 1;
|
|
8
|
-
transform: none;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
.landing-fu {
|
|
13
|
-
animation: landing-fu 0.5s cubic-bezier(0.2, 0.7, 0.3, 1) both;
|
|
14
|
-
}
|
|
1
|
+
@keyframes landing-fu {
|
|
2
|
+
from {
|
|
3
|
+
opacity: 0;
|
|
4
|
+
transform: translateY(10px);
|
|
5
|
+
}
|
|
6
|
+
to {
|
|
7
|
+
opacity: 1;
|
|
8
|
+
transform: none;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.landing-fu {
|
|
13
|
+
animation: landing-fu 0.5s cubic-bezier(0.2, 0.7, 0.3, 1) both;
|
|
14
|
+
}
|
package/src/landingModule.tsx
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import type { ResolvedLandingModule } from "@lukoweb/apitogo";
|
|
2
|
-
import type { NavigationPlugin } from "@lukoweb/apitogo/plugins";
|
|
3
|
-
import type { ComponentType, ReactNode } from "react";
|
|
4
|
-
import { isValidElement } from "react";
|
|
5
|
-
import type { RouteObject } from "react-router";
|
|
6
|
-
import { DefaultLandingPage } from "./DefaultLandingPage.js";
|
|
7
|
-
|
|
8
|
-
export type LandingModuleOptions = ResolvedLandingModule & {
|
|
9
|
-
component?: ComponentType | ReactNode;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const renderLandingElement = (options: LandingModuleOptions) => {
|
|
13
|
-
if (options.component) {
|
|
14
|
-
if (isValidElement(options.component)) {
|
|
15
|
-
return options.component;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const Component = options.component as ComponentType;
|
|
19
|
-
return <Component />;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return <DefaultLandingPage content={options.content} />;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export const landingModule = (
|
|
26
|
-
options: LandingModuleOptions,
|
|
27
|
-
): NavigationPlugin | undefined => {
|
|
28
|
-
if (!options.enabled) {
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const routes: RouteObject[] = [
|
|
33
|
-
{
|
|
34
|
-
path: options.path,
|
|
35
|
-
element: renderLandingElement(options),
|
|
36
|
-
handle: {
|
|
37
|
-
layout: options.layout,
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
getRoutes: () => routes,
|
|
44
|
-
getNavigation: async () => [],
|
|
45
|
-
};
|
|
46
|
-
};
|
|
1
|
+
import type { ResolvedLandingModule } from "@lukoweb/apitogo";
|
|
2
|
+
import type { NavigationPlugin } from "@lukoweb/apitogo/plugins";
|
|
3
|
+
import type { ComponentType, ReactNode } from "react";
|
|
4
|
+
import { isValidElement } from "react";
|
|
5
|
+
import type { RouteObject } from "react-router";
|
|
6
|
+
import { DefaultLandingPage } from "./DefaultLandingPage.js";
|
|
7
|
+
|
|
8
|
+
export type LandingModuleOptions = ResolvedLandingModule & {
|
|
9
|
+
component?: ComponentType | ReactNode;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const renderLandingElement = (options: LandingModuleOptions) => {
|
|
13
|
+
if (options.component) {
|
|
14
|
+
if (isValidElement(options.component)) {
|
|
15
|
+
return options.component;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const Component = options.component as ComponentType;
|
|
19
|
+
return <Component />;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return <DefaultLandingPage content={options.content} />;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const landingModule = (
|
|
26
|
+
options: LandingModuleOptions,
|
|
27
|
+
): NavigationPlugin | undefined => {
|
|
28
|
+
if (!options.enabled) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const routes: RouteObject[] = [
|
|
33
|
+
{
|
|
34
|
+
path: options.path,
|
|
35
|
+
element: renderLandingElement(options),
|
|
36
|
+
handle: {
|
|
37
|
+
layout: options.layout,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
getRoutes: () => routes,
|
|
44
|
+
getNavigation: async () => [],
|
|
45
|
+
};
|
|
46
|
+
};
|
|
@@ -1,131 +1,131 @@
|
|
|
1
|
-
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
2
|
-
import { cn } from "@lukoweb/apitogo";
|
|
3
|
-
import { Link } from "@lukoweb/apitogo/components";
|
|
4
|
-
import { Button } from "@lukoweb/apitogo/ui/Button.js";
|
|
5
|
-
import { ArrowRightIcon } from "lucide-react";
|
|
6
|
-
import { LandingIcon } from "./LandingIcon.js";
|
|
7
|
-
|
|
8
|
-
export type ApiShowcaseSectionProps = {
|
|
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";
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export const ApiShowcaseSection = ({
|
|
26
|
-
apiShowcase,
|
|
27
|
-
}: ApiShowcaseSectionProps) => {
|
|
28
|
-
const items = apiShowcase.items ?? [];
|
|
29
|
-
if (items.length === 0) {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const browseHref = apiShowcase.browseAllHref ?? "/api";
|
|
34
|
-
const browseLabel = apiShowcase.browseAllLabel ?? "Browse all APIs";
|
|
35
|
-
const layout = apiShowcase.layout ?? "grid";
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<section className="border-y border-border bg-background">
|
|
39
|
-
<div className="mx-auto max-w-[1200px] px-6 py-[3.75rem]">
|
|
40
|
-
<div className="mb-8 flex flex-wrap items-end justify-between gap-5">
|
|
41
|
-
<div>
|
|
42
|
-
{layout === "grid" ? (
|
|
43
|
-
<p className="mb-3 font-mono text-[0.8125rem] font-semibold text-primary">
|
|
44
|
-
{"// the catalog"}
|
|
45
|
-
</p>
|
|
46
|
-
) : null}
|
|
47
|
-
<h2 className="text-[2.125rem] leading-tight font-bold tracking-[-0.025em]">
|
|
48
|
-
{apiShowcase.title ?? "Production APIs. One key."}
|
|
49
|
-
</h2>
|
|
50
|
-
{apiShowcase.subtitle ? (
|
|
51
|
-
<p className="mt-2 text-muted-foreground">
|
|
52
|
-
{apiShowcase.subtitle}
|
|
53
|
-
</p>
|
|
54
|
-
) : null}
|
|
55
|
-
</div>
|
|
56
|
-
<Button asChild variant="outline" size="sm">
|
|
57
|
-
<Link to={browseHref}>
|
|
58
|
-
{browseLabel}
|
|
59
|
-
<ArrowRightIcon size={15} aria-hidden />
|
|
60
|
-
</Link>
|
|
61
|
-
</Button>
|
|
62
|
-
</div>
|
|
63
|
-
|
|
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}
|
|
95
|
-
</span>
|
|
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
|
-
)}
|
|
128
|
-
</div>
|
|
129
|
-
</section>
|
|
130
|
-
);
|
|
131
|
-
};
|
|
1
|
+
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
2
|
+
import { cn } from "@lukoweb/apitogo";
|
|
3
|
+
import { Link } from "@lukoweb/apitogo/components";
|
|
4
|
+
import { Button } from "@lukoweb/apitogo/ui/Button.js";
|
|
5
|
+
import { ArrowRightIcon } from "lucide-react";
|
|
6
|
+
import { LandingIcon } from "./LandingIcon.js";
|
|
7
|
+
|
|
8
|
+
export type ApiShowcaseSectionProps = {
|
|
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";
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const ApiShowcaseSection = ({
|
|
26
|
+
apiShowcase,
|
|
27
|
+
}: ApiShowcaseSectionProps) => {
|
|
28
|
+
const items = apiShowcase.items ?? [];
|
|
29
|
+
if (items.length === 0) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const browseHref = apiShowcase.browseAllHref ?? "/api";
|
|
34
|
+
const browseLabel = apiShowcase.browseAllLabel ?? "Browse all APIs";
|
|
35
|
+
const layout = apiShowcase.layout ?? "grid";
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<section className="border-y border-border bg-background">
|
|
39
|
+
<div className="mx-auto max-w-[1200px] px-6 py-[3.75rem]">
|
|
40
|
+
<div className="mb-8 flex flex-wrap items-end justify-between gap-5">
|
|
41
|
+
<div>
|
|
42
|
+
{layout === "grid" ? (
|
|
43
|
+
<p className="mb-3 font-mono text-[0.8125rem] font-semibold text-primary">
|
|
44
|
+
{"// the catalog"}
|
|
45
|
+
</p>
|
|
46
|
+
) : null}
|
|
47
|
+
<h2 className="text-[2.125rem] leading-tight font-bold tracking-[-0.025em]">
|
|
48
|
+
{apiShowcase.title ?? "Production APIs. One key."}
|
|
49
|
+
</h2>
|
|
50
|
+
{apiShowcase.subtitle ? (
|
|
51
|
+
<p className="mt-2 text-muted-foreground">
|
|
52
|
+
{apiShowcase.subtitle}
|
|
53
|
+
</p>
|
|
54
|
+
) : null}
|
|
55
|
+
</div>
|
|
56
|
+
<Button asChild variant="outline" size="sm">
|
|
57
|
+
<Link to={browseHref}>
|
|
58
|
+
{browseLabel}
|
|
59
|
+
<ArrowRightIcon size={15} aria-hidden />
|
|
60
|
+
</Link>
|
|
61
|
+
</Button>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
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}
|
|
95
|
+
</span>
|
|
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
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
</section>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
@@ -1,37 +1,37 @@
|
|
|
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 border-border bg-card shadow-sm">
|
|
16
|
-
<div className="flex items-center gap-2 border-b border-border bg-muted/40 px-4 py-3">
|
|
17
|
-
<span
|
|
18
|
-
className="size-[11px] rounded-full bg-destructive/80"
|
|
19
|
-
aria-hidden
|
|
20
|
-
/>
|
|
21
|
-
<span className="size-[11px] rounded-full bg-warning" aria-hidden />
|
|
22
|
-
<span className="size-[11px] rounded-full bg-primary" aria-hidden />
|
|
23
|
-
<span className="ms-2 font-mono text-xs text-muted-foreground">
|
|
24
|
-
{filename}
|
|
25
|
-
</span>
|
|
26
|
-
<TerminalIcon
|
|
27
|
-
size={14}
|
|
28
|
-
className="ms-auto text-muted-foreground/70"
|
|
29
|
-
aria-hidden
|
|
30
|
-
/>
|
|
31
|
-
</div>
|
|
32
|
-
<pre className="overflow-x-auto p-[1.125rem] font-mono text-[0.78125rem] leading-[1.85] text-foreground">
|
|
33
|
-
<code data-language={language}>{example.code}</code>
|
|
34
|
-
</pre>
|
|
35
|
-
</div>
|
|
36
|
-
);
|
|
37
|
-
};
|
|
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 border-border bg-card shadow-sm">
|
|
16
|
+
<div className="flex items-center gap-2 border-b border-border bg-muted/40 px-4 py-3">
|
|
17
|
+
<span
|
|
18
|
+
className="size-[11px] rounded-full bg-destructive/80"
|
|
19
|
+
aria-hidden
|
|
20
|
+
/>
|
|
21
|
+
<span className="size-[11px] rounded-full bg-warning" aria-hidden />
|
|
22
|
+
<span className="size-[11px] rounded-full bg-primary" aria-hidden />
|
|
23
|
+
<span className="ms-2 font-mono text-xs text-muted-foreground">
|
|
24
|
+
{filename}
|
|
25
|
+
</span>
|
|
26
|
+
<TerminalIcon
|
|
27
|
+
size={14}
|
|
28
|
+
className="ms-auto text-muted-foreground/70"
|
|
29
|
+
aria-hidden
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
<pre className="overflow-x-auto p-[1.125rem] font-mono text-[0.78125rem] leading-[1.85] text-foreground">
|
|
33
|
+
<code data-language={language}>{example.code}</code>
|
|
34
|
+
</pre>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
2
|
-
import { LandingIcon } from "./LandingIcon.js";
|
|
3
|
-
|
|
4
|
-
export type FeaturesSectionProps = {
|
|
5
|
-
features: NonNullable<ResolvedLandingContent["features"]>;
|
|
6
|
-
section?: ResolvedLandingContent["featuresSection"];
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export const FeaturesSection = ({
|
|
10
|
-
features,
|
|
11
|
-
section,
|
|
12
|
-
}: FeaturesSectionProps) => {
|
|
13
|
-
if (features.length === 0) {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const label = section?.label ?? "// why us";
|
|
18
|
-
const title = section?.title ?? "Everything you need to build on your API";
|
|
19
|
-
const description =
|
|
20
|
-
section?.description ??
|
|
21
|
-
"Skip the infra. Focus on your product while your developer portal handles docs, auth, and billing.";
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<section
|
|
25
|
-
id="features"
|
|
26
|
-
className="mx-auto max-w-[1200px] scroll-mt-(--top-header-height) px-6 py-10 pb-16"
|
|
27
|
-
>
|
|
28
|
-
<div className="mb-11 max-w-[640px]">
|
|
29
|
-
<p className="mb-3 font-mono text-[0.8125rem] font-semibold text-primary">
|
|
30
|
-
{label}
|
|
31
|
-
</p>
|
|
32
|
-
<h2 className="mb-3.5 text-[2.375rem] leading-tight font-bold tracking-[-0.025em] text-balance">
|
|
33
|
-
{title}
|
|
34
|
-
</h2>
|
|
35
|
-
<p className="text-[1.0625rem] leading-relaxed text-muted-foreground">
|
|
36
|
-
{description}
|
|
37
|
-
</p>
|
|
38
|
-
</div>
|
|
39
|
-
|
|
40
|
-
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
41
|
-
{features.map((feature) => (
|
|
42
|
-
<article
|
|
43
|
-
key={feature.title}
|
|
44
|
-
className="rounded-[14px] border border-border bg-card p-6 transition-colors hover:border-muted-foreground/30"
|
|
45
|
-
>
|
|
46
|
-
<div className="mb-4 grid size-[2.625rem] place-items-center rounded-[11px] bg-primary/10 text-primary">
|
|
47
|
-
<LandingIcon name={feature.icon} size={21} />
|
|
48
|
-
</div>
|
|
49
|
-
<h3 className="mb-2 text-[1.09375rem] font-semibold">
|
|
50
|
-
{feature.title}
|
|
51
|
-
</h3>
|
|
52
|
-
<p className="text-[0.90625rem] leading-relaxed text-muted-foreground">
|
|
53
|
-
{feature.description}
|
|
54
|
-
</p>
|
|
55
|
-
</article>
|
|
56
|
-
))}
|
|
57
|
-
</div>
|
|
58
|
-
</section>
|
|
59
|
-
);
|
|
60
|
-
};
|
|
1
|
+
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
2
|
+
import { LandingIcon } from "./LandingIcon.js";
|
|
3
|
+
|
|
4
|
+
export type FeaturesSectionProps = {
|
|
5
|
+
features: NonNullable<ResolvedLandingContent["features"]>;
|
|
6
|
+
section?: ResolvedLandingContent["featuresSection"];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const FeaturesSection = ({
|
|
10
|
+
features,
|
|
11
|
+
section,
|
|
12
|
+
}: FeaturesSectionProps) => {
|
|
13
|
+
if (features.length === 0) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const label = section?.label ?? "// why us";
|
|
18
|
+
const title = section?.title ?? "Everything you need to build on your API";
|
|
19
|
+
const description =
|
|
20
|
+
section?.description ??
|
|
21
|
+
"Skip the infra. Focus on your product while your developer portal handles docs, auth, and billing.";
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<section
|
|
25
|
+
id="features"
|
|
26
|
+
className="mx-auto max-w-[1200px] scroll-mt-(--top-header-height) px-6 py-10 pb-16"
|
|
27
|
+
>
|
|
28
|
+
<div className="mb-11 max-w-[640px]">
|
|
29
|
+
<p className="mb-3 font-mono text-[0.8125rem] font-semibold text-primary">
|
|
30
|
+
{label}
|
|
31
|
+
</p>
|
|
32
|
+
<h2 className="mb-3.5 text-[2.375rem] leading-tight font-bold tracking-[-0.025em] text-balance">
|
|
33
|
+
{title}
|
|
34
|
+
</h2>
|
|
35
|
+
<p className="text-[1.0625rem] leading-relaxed text-muted-foreground">
|
|
36
|
+
{description}
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
41
|
+
{features.map((feature) => (
|
|
42
|
+
<article
|
|
43
|
+
key={feature.title}
|
|
44
|
+
className="rounded-[14px] border border-border bg-card p-6 transition-colors hover:border-muted-foreground/30"
|
|
45
|
+
>
|
|
46
|
+
<div className="mb-4 grid size-[2.625rem] place-items-center rounded-[11px] bg-primary/10 text-primary">
|
|
47
|
+
<LandingIcon name={feature.icon} size={21} />
|
|
48
|
+
</div>
|
|
49
|
+
<h3 className="mb-2 text-[1.09375rem] font-semibold">
|
|
50
|
+
{feature.title}
|
|
51
|
+
</h3>
|
|
52
|
+
<p className="text-[0.90625rem] leading-relaxed text-muted-foreground">
|
|
53
|
+
{feature.description}
|
|
54
|
+
</p>
|
|
55
|
+
</article>
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|
|
59
|
+
);
|
|
60
|
+
};
|