@lukoweb/apitogo-module-landing 0.1.58 → 0.1.60
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 +1 -0
- package/src/LandingFooterSlot.tsx +21 -0
- package/src/LandingShell.tsx +30 -15
- package/src/index.tsx +4 -0
- package/src/landing.css +1 -252
- package/src/sections/ApiShowcaseSection.tsx +22 -16
- package/src/sections/CodeExampleCard.tsx +14 -11
- package/src/sections/FeaturesSection.tsx +12 -7
- package/src/sections/FooterSection.tsx +150 -0
- package/src/sections/HeroSection.tsx +10 -16
- package/src/sections/LandingButtons.tsx +8 -9
- package/src/sections/PricingCtaSection.tsx +2 -2
- package/src/sections/TrustSection.tsx +5 -2
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.60",
|
|
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.60",
|
|
18
18
|
"react": "19.2.4",
|
|
19
19
|
"react-dom": "19.2.4",
|
|
20
20
|
"typescript": "5.9.3"
|
|
@@ -45,6 +45,7 @@ export const DefaultLandingPage = ({ content }: DefaultLandingPageProps) => {
|
|
|
45
45
|
{content.pricingCta ? (
|
|
46
46
|
<PricingCtaSection pricingCta={content.pricingCta} />
|
|
47
47
|
) : null}
|
|
48
|
+
{/* content.footer renders portal-wide via the layout "footer" slot. */}
|
|
48
49
|
</LandingShell>
|
|
49
50
|
);
|
|
50
51
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
2
|
+
import type { ComponentType } from "react";
|
|
3
|
+
import { FooterSection } from "./sections/FooterSection.js";
|
|
4
|
+
|
|
5
|
+
export type LandingFooterSlotConfig = ResolvedLandingContent["footer"];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Builds a component for the layout "footer" slot so the landing footer
|
|
9
|
+
* renders on every page (home, docs, API reference, pricing, auth).
|
|
10
|
+
*/
|
|
11
|
+
export const createLandingFooterSlot = (
|
|
12
|
+
footer: LandingFooterSlotConfig,
|
|
13
|
+
): ComponentType | undefined => {
|
|
14
|
+
if (!footer || footer.enabled === false) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const LandingFooterSlot = () => <FooterSection footer={footer} />;
|
|
19
|
+
|
|
20
|
+
return LandingFooterSlot;
|
|
21
|
+
};
|
package/src/LandingShell.tsx
CHANGED
|
@@ -1,15 +1,38 @@
|
|
|
1
1
|
import { cn } from "@lukoweb/apitogo";
|
|
2
2
|
import { Head, useZudoku } from "@lukoweb/apitogo/components";
|
|
3
|
-
import type
|
|
3
|
+
import { Children, isValidElement, type ReactNode } from "react";
|
|
4
|
+
import { FooterSection } from "./sections/FooterSection.js";
|
|
4
5
|
import "./landing.css";
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Detects a composed `FooterSection` in the element tree so the
|
|
9
|
+
* "Built with APIToGo" line can be replaced by the real footer.
|
|
10
|
+
*/
|
|
11
|
+
const containsFooterSection = (node: ReactNode): boolean =>
|
|
12
|
+
Children.toArray(node).some((child) => {
|
|
13
|
+
if (!isValidElement(child)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (child.type === FooterSection) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { children } = child.props as { children?: ReactNode };
|
|
22
|
+
return children ? containsFooterSection(children) : false;
|
|
23
|
+
});
|
|
24
|
+
|
|
6
25
|
export type LandingShellProps = {
|
|
7
26
|
children: ReactNode;
|
|
8
27
|
/** Page title for `<Head>`. Defaults to site title from config. */
|
|
9
28
|
title?: string;
|
|
10
29
|
/** Meta description for `<Head>`. */
|
|
11
30
|
description?: string;
|
|
12
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Show the "Built with APIToGo" line. Defaults to true.
|
|
33
|
+
* Automatically hidden when a `FooterSection` is composed as a child or
|
|
34
|
+
* a footer slot is configured (e.g. `modules.landing.content.footer`).
|
|
35
|
+
*/
|
|
13
36
|
showPoweredBy?: boolean;
|
|
14
37
|
};
|
|
15
38
|
|
|
@@ -23,30 +46,22 @@ export const LandingShell = ({
|
|
|
23
46
|
const siteTitle =
|
|
24
47
|
options.metadata?.applicationName ?? options.site?.title ?? "APIToGo";
|
|
25
48
|
const pageTitle = title ?? siteTitle;
|
|
49
|
+
const hasFooter =
|
|
50
|
+
containsFooterSection(children) || Boolean(options.slots?.footer);
|
|
26
51
|
|
|
27
52
|
return (
|
|
28
|
-
<div className="
|
|
53
|
+
<div className="relative overflow-hidden bg-background text-foreground">
|
|
29
54
|
<Head>
|
|
30
|
-
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
31
|
-
<link
|
|
32
|
-
rel="preconnect"
|
|
33
|
-
href="https://fonts.gstatic.com"
|
|
34
|
-
crossOrigin=""
|
|
35
|
-
/>
|
|
36
|
-
<link
|
|
37
|
-
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
|
|
38
|
-
rel="stylesheet"
|
|
39
|
-
/>
|
|
40
55
|
<title>{pageTitle}</title>
|
|
41
56
|
{description ? <meta name="description" content={description} /> : null}
|
|
42
57
|
</Head>
|
|
43
58
|
|
|
44
59
|
{children}
|
|
45
60
|
|
|
46
|
-
{showPoweredBy ? (
|
|
61
|
+
{showPoweredBy && !hasFooter ? (
|
|
47
62
|
<div
|
|
48
63
|
className={cn(
|
|
49
|
-
"
|
|
64
|
+
"border-t border-border py-6 text-center text-xs text-muted-foreground/70",
|
|
50
65
|
!options.site?.footer && "mb-8",
|
|
51
66
|
)}
|
|
52
67
|
>
|
package/src/index.tsx
CHANGED
|
@@ -4,6 +4,8 @@ export { LandingShell } from "./LandingShell.js";
|
|
|
4
4
|
export type { LandingShellProps } from "./LandingShell.js";
|
|
5
5
|
export { landingModule } from "./landingModule.js";
|
|
6
6
|
export type { LandingModuleOptions } from "./landingModule.js";
|
|
7
|
+
export { createLandingFooterSlot } from "./LandingFooterSlot.js";
|
|
8
|
+
export type { LandingFooterSlotConfig } from "./LandingFooterSlot.js";
|
|
7
9
|
export { HeroSection } from "./sections/HeroSection.js";
|
|
8
10
|
export type { HeroSectionProps } from "./sections/HeroSection.js";
|
|
9
11
|
export { TrustSection } from "./sections/TrustSection.js";
|
|
@@ -14,5 +16,7 @@ export { ApiShowcaseSection } from "./sections/ApiShowcaseSection.js";
|
|
|
14
16
|
export type { ApiShowcaseSectionProps } from "./sections/ApiShowcaseSection.js";
|
|
15
17
|
export { PricingCtaSection } from "./sections/PricingCtaSection.js";
|
|
16
18
|
export type { PricingCtaSectionProps } from "./sections/PricingCtaSection.js";
|
|
19
|
+
export { FooterSection } from "./sections/FooterSection.js";
|
|
20
|
+
export type { FooterSectionProps } from "./sections/FooterSection.js";
|
|
17
21
|
export { CodeExampleCard } from "./sections/CodeExampleCard.js";
|
|
18
22
|
export type { CodeExampleCardProps } from "./sections/CodeExampleCard.js";
|
package/src/landing.css
CHANGED
|
@@ -1,52 +1,3 @@
|
|
|
1
|
-
.apitogo-landing {
|
|
2
|
-
--landing-display: "Space Grotesk", var(--font-sans, system-ui), sans-serif;
|
|
3
|
-
--landing-body: "IBM Plex Sans", var(--font-sans, system-ui), sans-serif;
|
|
4
|
-
--landing-mono: "JetBrains Mono", var(--font-mono, ui-monospace), monospace;
|
|
5
|
-
--landing-bg: #0b0d12;
|
|
6
|
-
--landing-bg-2: #0e1119;
|
|
7
|
-
--landing-surface: #14171f;
|
|
8
|
-
--landing-surface-2: #191d27;
|
|
9
|
-
--landing-border: #252b37;
|
|
10
|
-
--landing-border-2: #333a48;
|
|
11
|
-
--landing-text: #edeff5;
|
|
12
|
-
--landing-text-2: #a6aebf;
|
|
13
|
-
--landing-text-3: #6b7385;
|
|
14
|
-
--landing-accent: #f23645;
|
|
15
|
-
--landing-accent-hover: #ff515d;
|
|
16
|
-
--landing-accent-soft: rgba(242, 54, 69, 0.13);
|
|
17
|
-
--landing-on-accent: #fff;
|
|
18
|
-
--landing-green: #16c784;
|
|
19
|
-
--landing-green-soft: rgba(22, 199, 132, 0.13);
|
|
20
|
-
--landing-peri: #aeb9e8;
|
|
21
|
-
--landing-code-bg: #0c0f16;
|
|
22
|
-
--landing-shadow:
|
|
23
|
-
0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px -8px rgba(0, 0, 0, 0.55);
|
|
24
|
-
background: var(--landing-bg);
|
|
25
|
-
color: var(--landing-text);
|
|
26
|
-
font-family: var(--landing-body);
|
|
27
|
-
-webkit-font-smoothing: antialiased;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
:not(.dark) .apitogo-landing {
|
|
31
|
-
--landing-bg: #f5f6f9;
|
|
32
|
-
--landing-bg-2: #ffffff;
|
|
33
|
-
--landing-surface: #ffffff;
|
|
34
|
-
--landing-surface-2: #fafbfc;
|
|
35
|
-
--landing-border: #e6e8ef;
|
|
36
|
-
--landing-border-2: #d6dae3;
|
|
37
|
-
--landing-text: #0e1016;
|
|
38
|
-
--landing-text-2: #565e70;
|
|
39
|
-
--landing-text-3: #8a92a3;
|
|
40
|
-
--landing-accent: #e11d2a;
|
|
41
|
-
--landing-accent-hover: #c2151f;
|
|
42
|
-
--landing-accent-soft: rgba(225, 29, 42, 0.08);
|
|
43
|
-
--landing-green: #0e9e63;
|
|
44
|
-
--landing-green-soft: rgba(14, 158, 99, 0.1);
|
|
45
|
-
--landing-peri: #4e5ac0;
|
|
46
|
-
--landing-shadow:
|
|
47
|
-
0 1px 2px rgba(16, 24, 40, 0.05), 0 8px 24px -10px rgba(16, 24, 40, 0.14);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
1
|
@keyframes landing-fu {
|
|
51
2
|
from {
|
|
52
3
|
opacity: 0;
|
|
@@ -58,208 +9,6 @@
|
|
|
58
9
|
}
|
|
59
10
|
}
|
|
60
11
|
|
|
61
|
-
.
|
|
62
|
-
font-family: var(--landing-display);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.apitogo-landing .landing-mono {
|
|
66
|
-
font-family: var(--landing-mono);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
.apitogo-landing .landing-fu {
|
|
12
|
+
.landing-fu {
|
|
70
13
|
animation: landing-fu 0.5s cubic-bezier(0.2, 0.7, 0.3, 1) both;
|
|
71
14
|
}
|
|
72
|
-
|
|
73
|
-
.apitogo-landing .landing-section-label {
|
|
74
|
-
color: var(--landing-accent);
|
|
75
|
-
font-family: var(--landing-mono);
|
|
76
|
-
font-size: 0.8125rem;
|
|
77
|
-
font-weight: 600;
|
|
78
|
-
margin-bottom: 0.75rem;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.apitogo-landing .landing-muted {
|
|
82
|
-
color: var(--landing-text-2);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
.apitogo-landing .landing-muted-3 {
|
|
86
|
-
color: var(--landing-text-3);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
.apitogo-landing .landing-btn-primary {
|
|
90
|
-
align-items: center;
|
|
91
|
-
background: var(--landing-accent);
|
|
92
|
-
border: none;
|
|
93
|
-
border-radius: 11px;
|
|
94
|
-
box-shadow: 0 8px 24px -8px var(--landing-accent-soft);
|
|
95
|
-
color: var(--landing-on-accent);
|
|
96
|
-
cursor: pointer;
|
|
97
|
-
display: inline-flex;
|
|
98
|
-
font-family: inherit;
|
|
99
|
-
font-size: 0.96875rem;
|
|
100
|
-
font-weight: 600;
|
|
101
|
-
gap: 0.5rem;
|
|
102
|
-
padding: 0.8125rem 1.375rem;
|
|
103
|
-
text-decoration: none;
|
|
104
|
-
transition: background 0.15s ease;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
.apitogo-landing .landing-btn-primary:hover {
|
|
108
|
-
background: var(--landing-accent-hover);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
.apitogo-landing .landing-btn-secondary {
|
|
112
|
-
align-items: center;
|
|
113
|
-
background: var(--landing-surface);
|
|
114
|
-
border: 1px solid var(--landing-border);
|
|
115
|
-
border-radius: 11px;
|
|
116
|
-
color: var(--landing-text);
|
|
117
|
-
cursor: pointer;
|
|
118
|
-
display: inline-flex;
|
|
119
|
-
font-family: inherit;
|
|
120
|
-
font-size: 0.96875rem;
|
|
121
|
-
font-weight: 600;
|
|
122
|
-
gap: 0.5rem;
|
|
123
|
-
padding: 0.8125rem 1.375rem;
|
|
124
|
-
text-decoration: none;
|
|
125
|
-
transition:
|
|
126
|
-
border-color 0.15s ease,
|
|
127
|
-
background 0.15s ease;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.apitogo-landing .landing-btn-secondary:hover {
|
|
131
|
-
border-color: var(--landing-border-2);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
.apitogo-landing .landing-btn-sm {
|
|
135
|
-
border-radius: 9px;
|
|
136
|
-
font-size: 0.875rem;
|
|
137
|
-
padding: 0.625rem 1rem;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
.apitogo-landing .landing-hero-gradient {
|
|
141
|
-
background: radial-gradient(
|
|
142
|
-
1000px 480px at 78% -8%,
|
|
143
|
-
var(--landing-accent-soft),
|
|
144
|
-
transparent 70%
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
.apitogo-landing .landing-badge {
|
|
149
|
-
align-items: center;
|
|
150
|
-
background: var(--landing-surface);
|
|
151
|
-
border: 1px solid var(--landing-border);
|
|
152
|
-
border-radius: 999px;
|
|
153
|
-
color: var(--landing-text-2);
|
|
154
|
-
display: inline-flex;
|
|
155
|
-
font-size: 0.78125rem;
|
|
156
|
-
gap: 0.5rem;
|
|
157
|
-
margin-bottom: 1.375rem;
|
|
158
|
-
padding: 0.375rem 0.75rem 0.375rem 0.5rem;
|
|
159
|
-
width: fit-content;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
.apitogo-landing .landing-status-pill {
|
|
163
|
-
align-items: center;
|
|
164
|
-
background: var(--landing-green-soft);
|
|
165
|
-
border-radius: 999px;
|
|
166
|
-
color: var(--landing-green);
|
|
167
|
-
display: inline-flex;
|
|
168
|
-
font-family: var(--landing-mono);
|
|
169
|
-
font-size: 0.6875rem;
|
|
170
|
-
font-weight: 600;
|
|
171
|
-
gap: 0.3125rem;
|
|
172
|
-
padding: 0.125rem 0.5rem;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
.apitogo-landing .landing-status-dot {
|
|
176
|
-
background: var(--landing-green);
|
|
177
|
-
border-radius: 50%;
|
|
178
|
-
height: 6px;
|
|
179
|
-
width: 6px;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
.apitogo-landing .landing-feature-card {
|
|
183
|
-
background: var(--landing-surface);
|
|
184
|
-
border: 1px solid var(--landing-border);
|
|
185
|
-
border-radius: 14px;
|
|
186
|
-
padding: 1.5rem;
|
|
187
|
-
transition: border-color 0.15s ease;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
.apitogo-landing .landing-feature-card:hover {
|
|
191
|
-
border-color: var(--landing-border-2);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
.apitogo-landing .landing-feature-icon {
|
|
195
|
-
background: var(--landing-accent-soft);
|
|
196
|
-
border-radius: 11px;
|
|
197
|
-
color: var(--landing-accent);
|
|
198
|
-
display: grid;
|
|
199
|
-
height: 2.625rem;
|
|
200
|
-
margin-bottom: 1rem;
|
|
201
|
-
place-items: center;
|
|
202
|
-
width: 2.625rem;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
.apitogo-landing .landing-api-card {
|
|
206
|
-
background: var(--landing-surface);
|
|
207
|
-
border: 1px solid var(--landing-border);
|
|
208
|
-
border-radius: 13px;
|
|
209
|
-
color: var(--landing-text);
|
|
210
|
-
cursor: pointer;
|
|
211
|
-
padding: 1.125rem;
|
|
212
|
-
text-decoration: none;
|
|
213
|
-
transition:
|
|
214
|
-
border-color 0.15s ease,
|
|
215
|
-
transform 0.15s ease;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
.apitogo-landing .landing-api-card:hover {
|
|
219
|
-
border-color: var(--landing-border-2);
|
|
220
|
-
transform: translateY(-2px);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
.apitogo-landing .landing-api-icon {
|
|
224
|
-
background: var(--landing-surface-2);
|
|
225
|
-
border: 1px solid var(--landing-border);
|
|
226
|
-
border-radius: 9px;
|
|
227
|
-
color: var(--landing-peri);
|
|
228
|
-
display: grid;
|
|
229
|
-
height: 2.25rem;
|
|
230
|
-
place-items: center;
|
|
231
|
-
width: 2.25rem;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
.apitogo-landing .landing-latency-badge {
|
|
235
|
-
background: var(--landing-green-soft);
|
|
236
|
-
border-radius: 6px;
|
|
237
|
-
color: var(--landing-green);
|
|
238
|
-
font-family: var(--landing-mono);
|
|
239
|
-
font-size: 0.6875rem;
|
|
240
|
-
font-weight: 600;
|
|
241
|
-
padding: 0.1875rem 0.4375rem;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
.apitogo-landing .landing-trust-logo {
|
|
245
|
-
color: var(--landing-text-2);
|
|
246
|
-
font-family: var(--landing-display);
|
|
247
|
-
font-size: 1.1875rem;
|
|
248
|
-
font-weight: 600;
|
|
249
|
-
letter-spacing: -0.01em;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
.apitogo-landing .landing-showcase-section {
|
|
253
|
-
background: var(--landing-bg-2);
|
|
254
|
-
border-bottom: 1px solid var(--landing-border);
|
|
255
|
-
border-top: 1px solid var(--landing-border);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
.apitogo-landing .landing-hero-section {
|
|
259
|
-
border-bottom: 1px solid var(--landing-border);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
.apitogo-landing .landing-powered-by {
|
|
263
|
-
border-top: 1px solid var(--landing-border);
|
|
264
|
-
color: var(--landing-text-3);
|
|
265
|
-
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
2
2
|
import { Link } from "@lukoweb/apitogo/components";
|
|
3
|
+
import { Button } from "@lukoweb/apitogo/ui/Button.js";
|
|
3
4
|
import { ArrowRightIcon } from "lucide-react";
|
|
4
5
|
import { LandingIcon } from "./LandingIcon.js";
|
|
5
6
|
|
|
@@ -19,25 +20,28 @@ export const ApiShowcaseSection = ({
|
|
|
19
20
|
const browseLabel = apiShowcase.browseAllLabel ?? "Browse all APIs";
|
|
20
21
|
|
|
21
22
|
return (
|
|
22
|
-
<section className="
|
|
23
|
+
<section className="border-y border-border bg-muted/30">
|
|
23
24
|
<div className="mx-auto max-w-[1200px] px-6 py-[3.75rem]">
|
|
24
25
|
<div className="mb-8 flex flex-wrap items-end justify-between gap-5">
|
|
25
26
|
<div>
|
|
26
|
-
<p className="
|
|
27
|
-
|
|
27
|
+
<p className="mb-3 font-mono text-[0.8125rem] font-semibold text-primary">
|
|
28
|
+
{"// the catalog"}
|
|
29
|
+
</p>
|
|
30
|
+
<h2 className="text-[2.125rem] leading-tight font-bold tracking-[-0.025em]">
|
|
28
31
|
{apiShowcase.title ?? "Production APIs. One key."}
|
|
29
32
|
</h2>
|
|
30
33
|
{apiShowcase.subtitle ? (
|
|
31
|
-
<p className="
|
|
34
|
+
<p className="mt-2 text-muted-foreground">
|
|
35
|
+
{apiShowcase.subtitle}
|
|
36
|
+
</p>
|
|
32
37
|
) : null}
|
|
33
38
|
</div>
|
|
34
|
-
<
|
|
35
|
-
to={browseHref}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
</Link>
|
|
39
|
+
<Button asChild variant="outline" size="sm">
|
|
40
|
+
<Link to={browseHref}>
|
|
41
|
+
{browseLabel}
|
|
42
|
+
<ArrowRightIcon size={15} aria-hidden />
|
|
43
|
+
</Link>
|
|
44
|
+
</Button>
|
|
41
45
|
</div>
|
|
42
46
|
|
|
43
47
|
<div className="grid gap-3.5 sm:grid-cols-2 lg:grid-cols-4">
|
|
@@ -45,20 +49,22 @@ export const ApiShowcaseSection = ({
|
|
|
45
49
|
<Link
|
|
46
50
|
key={`${item.name}-${item.path}`}
|
|
47
51
|
to={browseHref}
|
|
48
|
-
className="
|
|
52
|
+
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"
|
|
49
53
|
>
|
|
50
54
|
<div className="mb-3.5 flex items-center justify-between">
|
|
51
|
-
<div className="
|
|
55
|
+
<div className="grid size-9 place-items-center rounded-[9px] border border-border bg-muted/40 text-primary">
|
|
52
56
|
<LandingIcon name={item.icon ?? "boxes"} size={18} />
|
|
53
57
|
</div>
|
|
54
58
|
{item.latency ? (
|
|
55
|
-
<span className="
|
|
59
|
+
<span className="rounded-md bg-primary/10 px-[0.4375rem] py-[0.1875rem] font-mono text-[0.6875rem] font-semibold text-primary">
|
|
60
|
+
{item.latency}
|
|
61
|
+
</span>
|
|
56
62
|
) : null}
|
|
57
63
|
</div>
|
|
58
|
-
<h3 className="
|
|
64
|
+
<h3 className="mb-1 text-[0.9375rem] font-semibold">
|
|
59
65
|
{item.name}
|
|
60
66
|
</h3>
|
|
61
|
-
<p className="
|
|
67
|
+
<p className="font-mono text-[0.71875rem] text-muted-foreground/70">
|
|
62
68
|
{item.path}
|
|
63
69
|
</p>
|
|
64
70
|
</Link>
|
|
@@ -12,24 +12,27 @@ export const CodeExampleCard = ({ example }: CodeExampleCardProps) => {
|
|
|
12
12
|
const language = example.language ?? "bash";
|
|
13
13
|
|
|
14
14
|
return (
|
|
15
|
-
<div
|
|
16
|
-
className="
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
<span
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
22
|
+
className="size-[11px] rounded-full bg-warning"
|
|
23
|
+
aria-hidden
|
|
24
|
+
/>
|
|
25
|
+
<span className="size-[11px] rounded-full bg-primary" aria-hidden />
|
|
26
|
+
<span className="ms-2 font-mono text-xs text-muted-foreground">
|
|
24
27
|
{filename}
|
|
25
28
|
</span>
|
|
26
29
|
<TerminalIcon
|
|
27
30
|
size={14}
|
|
28
|
-
className="ms-auto text-
|
|
31
|
+
className="ms-auto text-muted-foreground/70"
|
|
29
32
|
aria-hidden
|
|
30
33
|
/>
|
|
31
34
|
</div>
|
|
32
|
-
<pre className="
|
|
35
|
+
<pre className="overflow-x-auto p-[1.125rem] font-mono text-[0.78125rem] leading-[1.85] text-foreground">
|
|
33
36
|
<code data-language={language}>{example.code}</code>
|
|
34
37
|
</pre>
|
|
35
38
|
</div>
|
|
@@ -23,25 +23,30 @@ export const FeaturesSection = ({
|
|
|
23
23
|
return (
|
|
24
24
|
<section className="mx-auto max-w-[1200px] px-6 py-10 pb-16">
|
|
25
25
|
<div className="mb-11 max-w-[640px]">
|
|
26
|
-
<p className="
|
|
27
|
-
|
|
26
|
+
<p className="mb-3 font-mono text-[0.8125rem] font-semibold text-primary">
|
|
27
|
+
{label}
|
|
28
|
+
</p>
|
|
29
|
+
<h2 className="mb-3.5 text-[2.375rem] leading-tight font-bold tracking-[-0.025em] text-balance">
|
|
28
30
|
{title}
|
|
29
31
|
</h2>
|
|
30
|
-
<p className="
|
|
32
|
+
<p className="text-[1.0625rem] leading-relaxed text-muted-foreground">
|
|
31
33
|
{description}
|
|
32
34
|
</p>
|
|
33
35
|
</div>
|
|
34
36
|
|
|
35
37
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
36
38
|
{features.map((feature) => (
|
|
37
|
-
<article
|
|
38
|
-
|
|
39
|
+
<article
|
|
40
|
+
key={feature.title}
|
|
41
|
+
className="rounded-[14px] border border-border bg-card p-6 transition-colors hover:border-muted-foreground/30"
|
|
42
|
+
>
|
|
43
|
+
<div className="mb-4 grid size-[2.625rem] place-items-center rounded-[11px] bg-primary/10 text-primary">
|
|
39
44
|
<LandingIcon name={feature.icon} size={21} />
|
|
40
45
|
</div>
|
|
41
|
-
<h3 className="
|
|
46
|
+
<h3 className="mb-2 text-[1.09375rem] font-semibold">
|
|
42
47
|
{feature.title}
|
|
43
48
|
</h3>
|
|
44
|
-
<p className="
|
|
49
|
+
<p className="text-[0.90625rem] leading-relaxed text-muted-foreground">
|
|
45
50
|
{feature.description}
|
|
46
51
|
</p>
|
|
47
52
|
</article>
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
2
|
+
import { useZudoku } from "@lukoweb/apitogo/components";
|
|
3
|
+
import type { ReactElement } from "react";
|
|
4
|
+
|
|
5
|
+
export type FooterSectionProps = {
|
|
6
|
+
footer: NonNullable<ResolvedLandingContent["footer"]>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const GitHubIcon = ({ className }: { className?: string }) => (
|
|
10
|
+
<svg
|
|
11
|
+
viewBox="0 0 24 24"
|
|
12
|
+
fill="currentColor"
|
|
13
|
+
className={className}
|
|
14
|
+
aria-hidden
|
|
15
|
+
>
|
|
16
|
+
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12Z" />
|
|
17
|
+
</svg>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const XIcon = ({ className }: { className?: string }) => (
|
|
21
|
+
<svg
|
|
22
|
+
viewBox="0 0 24 24"
|
|
23
|
+
fill="currentColor"
|
|
24
|
+
className={className}
|
|
25
|
+
aria-hidden
|
|
26
|
+
>
|
|
27
|
+
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
|
28
|
+
</svg>
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const SOCIAL_ICONS: Record<
|
|
32
|
+
string,
|
|
33
|
+
(props: { className?: string }) => ReactElement
|
|
34
|
+
> = {
|
|
35
|
+
github: GitHubIcon,
|
|
36
|
+
x: XIcon,
|
|
37
|
+
twitter: XIcon,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const FooterSocialLink = ({
|
|
41
|
+
icon,
|
|
42
|
+
href,
|
|
43
|
+
label,
|
|
44
|
+
}: {
|
|
45
|
+
icon: string;
|
|
46
|
+
href: string;
|
|
47
|
+
label?: string;
|
|
48
|
+
}) => {
|
|
49
|
+
const Icon = SOCIAL_ICONS[icon.toLowerCase()];
|
|
50
|
+
const accessibleLabel = label ?? icon;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<a
|
|
54
|
+
href={href}
|
|
55
|
+
target="_blank"
|
|
56
|
+
rel="noopener noreferrer"
|
|
57
|
+
aria-label={accessibleLabel}
|
|
58
|
+
className="inline-flex items-center justify-center text-muted-foreground/70 transition-colors hover:text-foreground"
|
|
59
|
+
>
|
|
60
|
+
{Icon ? (
|
|
61
|
+
<Icon className="h-[1.125rem] w-[1.125rem]" />
|
|
62
|
+
) : (
|
|
63
|
+
<span className="text-xs font-medium">{accessibleLabel}</span>
|
|
64
|
+
)}
|
|
65
|
+
</a>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const FooterSection = ({ footer }: FooterSectionProps) => {
|
|
70
|
+
const { options } = useZudoku();
|
|
71
|
+
const siteTitle =
|
|
72
|
+
options.metadata?.applicationName ?? options.site?.title ?? "APIToGo";
|
|
73
|
+
|
|
74
|
+
const brandName = footer.brand?.name ?? siteTitle;
|
|
75
|
+
const tagline = footer.brand?.tagline;
|
|
76
|
+
const logoSrc = footer.brand?.logoSrc;
|
|
77
|
+
const columns = footer.columns ?? [];
|
|
78
|
+
const social = footer.social ?? [];
|
|
79
|
+
const year = new Date().getFullYear();
|
|
80
|
+
const copyright =
|
|
81
|
+
footer.copyright ?? `© ${year} ${brandName}. All rights reserved.`;
|
|
82
|
+
|
|
83
|
+
if (columns.length === 0 && !tagline && !logoSrc && social.length === 0) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<footer className="border-t bg-background">
|
|
89
|
+
<div className="mx-auto max-w-[1200px] px-6 py-12">
|
|
90
|
+
<div className="grid gap-10 sm:grid-cols-2 lg:grid-cols-[1.4fr_repeat(3,minmax(0,1fr))]">
|
|
91
|
+
<div className="sm:col-span-2 lg:col-span-1">
|
|
92
|
+
<div className="mb-3 flex items-center gap-2.5">
|
|
93
|
+
{logoSrc ? (
|
|
94
|
+
<img
|
|
95
|
+
src={logoSrc}
|
|
96
|
+
alt={brandName}
|
|
97
|
+
className="h-7 w-auto object-contain"
|
|
98
|
+
/>
|
|
99
|
+
) : (
|
|
100
|
+
<span className="text-[1.125rem] font-bold tracking-[-0.02em]">
|
|
101
|
+
<span className="text-primary">API</span>
|
|
102
|
+
<span>ToGo</span>
|
|
103
|
+
</span>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
{tagline ? (
|
|
107
|
+
<p className="max-w-[280px] text-[0.875rem] leading-relaxed text-muted-foreground">
|
|
108
|
+
{tagline}
|
|
109
|
+
</p>
|
|
110
|
+
) : null}
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{columns.map((column) => (
|
|
114
|
+
<div key={column.title}>
|
|
115
|
+
<h3 className="mb-4 text-sm font-semibold">{column.title}</h3>
|
|
116
|
+
<ul className="space-y-2.5">
|
|
117
|
+
{column.links.map((link) => (
|
|
118
|
+
<li key={`${column.title}-${link.label}`}>
|
|
119
|
+
<a
|
|
120
|
+
href={link.href}
|
|
121
|
+
className="text-sm text-muted-foreground transition-colors hover:text-foreground"
|
|
122
|
+
>
|
|
123
|
+
{link.label}
|
|
124
|
+
</a>
|
|
125
|
+
</li>
|
|
126
|
+
))}
|
|
127
|
+
</ul>
|
|
128
|
+
</div>
|
|
129
|
+
))}
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div className="mt-10 flex flex-col items-center justify-between gap-4 border-t border-border pt-6 sm:flex-row">
|
|
133
|
+
<p className="text-sm text-muted-foreground/70">{copyright}</p>
|
|
134
|
+
{social.length > 0 ? (
|
|
135
|
+
<div className="flex items-center gap-4">
|
|
136
|
+
{social.map((item) => (
|
|
137
|
+
<FooterSocialLink
|
|
138
|
+
key={`${item.icon}-${item.href}`}
|
|
139
|
+
icon={item.icon}
|
|
140
|
+
href={item.href}
|
|
141
|
+
label={item.label}
|
|
142
|
+
/>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
) : null}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</footer>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
@@ -19,10 +19,10 @@ export const HeroSection = ({
|
|
|
19
19
|
const highlights = hero.highlights ?? [];
|
|
20
20
|
|
|
21
21
|
return (
|
|
22
|
-
<section className="
|
|
22
|
+
<section className="relative overflow-hidden border-b border-border">
|
|
23
23
|
<div
|
|
24
24
|
aria-hidden
|
|
25
|
-
className="
|
|
25
|
+
className="pointer-events-none absolute inset-0 bg-[radial-gradient(1000px_480px_at_78%_-8%,color-mix(in_oklch,var(--primary)_13%,transparent),transparent_70%)]"
|
|
26
26
|
/>
|
|
27
27
|
|
|
28
28
|
<div
|
|
@@ -39,10 +39,10 @@ export const HeroSection = ({
|
|
|
39
39
|
)}
|
|
40
40
|
>
|
|
41
41
|
{(hero.badge || hero.subtitle) && (
|
|
42
|
-
<div className="
|
|
42
|
+
<div className="mb-[1.375rem] inline-flex w-fit items-center gap-2 rounded-full border border-border bg-card px-3 py-1.5 text-[0.78125rem] text-muted-foreground">
|
|
43
43
|
{hero.badge ? (
|
|
44
|
-
<span className="
|
|
45
|
-
<span className="
|
|
44
|
+
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary/10 px-2 py-0.5 font-mono text-[0.6875rem] font-semibold text-primary">
|
|
45
|
+
<span className="size-1.5 rounded-full bg-primary" aria-hidden />
|
|
46
46
|
{hero.badge}
|
|
47
47
|
</span>
|
|
48
48
|
) : null}
|
|
@@ -50,20 +50,18 @@ export const HeroSection = ({
|
|
|
50
50
|
</div>
|
|
51
51
|
)}
|
|
52
52
|
|
|
53
|
-
<h1 className="
|
|
53
|
+
<h1 className="mb-5 text-[2.5rem] leading-[1.02] font-bold tracking-[-0.03em] text-balance sm:text-5xl lg:text-[3.5rem]">
|
|
54
54
|
{hero.title}
|
|
55
55
|
{hero.titleAccent ? (
|
|
56
56
|
<>
|
|
57
57
|
<br />
|
|
58
|
-
<span className="text-
|
|
59
|
-
{hero.titleAccent}
|
|
60
|
-
</span>
|
|
58
|
+
<span className="text-primary">{hero.titleAccent}</span>
|
|
61
59
|
</>
|
|
62
60
|
) : null}
|
|
63
61
|
</h1>
|
|
64
62
|
|
|
65
63
|
{hero.description ? (
|
|
66
|
-
<p className="
|
|
64
|
+
<p className="mb-[1.875rem] max-w-[480px] text-lg leading-relaxed text-pretty text-muted-foreground">
|
|
67
65
|
{hero.description}
|
|
68
66
|
</p>
|
|
69
67
|
) : null}
|
|
@@ -91,17 +89,13 @@ export const HeroSection = ({
|
|
|
91
89
|
)}
|
|
92
90
|
|
|
93
91
|
{highlights.length > 0 ? (
|
|
94
|
-
<ul className="
|
|
92
|
+
<ul className="mt-[1.875rem] flex flex-wrap items-center gap-x-[1.375rem] gap-y-2 text-[0.8125rem] text-muted-foreground/70">
|
|
95
93
|
{highlights.map((item) => (
|
|
96
94
|
<li
|
|
97
95
|
key={item}
|
|
98
96
|
className="inline-flex items-center gap-[0.4375rem]"
|
|
99
97
|
>
|
|
100
|
-
<CheckIcon
|
|
101
|
-
size={15}
|
|
102
|
-
className="text-[var(--landing-green)]"
|
|
103
|
-
aria-hidden
|
|
104
|
-
/>
|
|
98
|
+
<CheckIcon size={15} className="text-primary" aria-hidden />
|
|
105
99
|
{item}
|
|
106
100
|
</li>
|
|
107
101
|
))}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cn } from "@lukoweb/apitogo";
|
|
2
2
|
import { Link } from "@lukoweb/apitogo/components";
|
|
3
|
+
import { Button } from "@lukoweb/apitogo/ui/Button.js";
|
|
3
4
|
import type { LucideIcon } from "lucide-react";
|
|
4
5
|
import type { ReactNode } from "react";
|
|
5
6
|
|
|
@@ -33,15 +34,13 @@ export const LandingButton = ({
|
|
|
33
34
|
);
|
|
34
35
|
|
|
35
36
|
return (
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
className,
|
|
42
|
-
)}
|
|
37
|
+
<Button
|
|
38
|
+
asChild
|
|
39
|
+
variant={variant === "primary" ? "default" : "outline"}
|
|
40
|
+
size={size === "sm" ? "sm" : "lg"}
|
|
41
|
+
className={cn(className)}
|
|
43
42
|
>
|
|
44
|
-
{content}
|
|
45
|
-
</
|
|
43
|
+
<Link to={href}>{content}</Link>
|
|
44
|
+
</Button>
|
|
46
45
|
);
|
|
47
46
|
};
|
|
@@ -14,10 +14,10 @@ export const PricingCtaSection = ({ pricingCta }: PricingCtaSectionProps) => {
|
|
|
14
14
|
|
|
15
15
|
return (
|
|
16
16
|
<section className="mx-auto max-w-[1200px] px-6 py-[4.5rem] text-center">
|
|
17
|
-
<h2 className="
|
|
17
|
+
<h2 className="mb-4 text-[2.5rem] leading-tight font-bold tracking-[-0.03em] text-balance">
|
|
18
18
|
{title}
|
|
19
19
|
</h2>
|
|
20
|
-
<p className="
|
|
20
|
+
<p className="mx-auto mb-[1.875rem] max-w-[520px] text-[1.09375rem] leading-relaxed text-muted-foreground">
|
|
21
21
|
{description}
|
|
22
22
|
</p>
|
|
23
23
|
<div className="flex flex-wrap items-center justify-center gap-3">
|
|
@@ -13,13 +13,16 @@ export const TrustSection = ({ trust }: TrustSectionProps) => {
|
|
|
13
13
|
return (
|
|
14
14
|
<section className="mx-auto max-w-[1200px] px-6 py-10">
|
|
15
15
|
{trust.title ? (
|
|
16
|
-
<p className="
|
|
16
|
+
<p className="mb-[1.375rem] text-center text-[0.78125rem] font-semibold tracking-[0.08em] text-muted-foreground/70 uppercase">
|
|
17
17
|
{trust.title}
|
|
18
18
|
</p>
|
|
19
19
|
) : null}
|
|
20
20
|
<div className="flex flex-wrap items-center justify-center gap-x-[2.875rem] gap-y-3.5 opacity-70">
|
|
21
21
|
{logos.map((logo) => (
|
|
22
|
-
<span
|
|
22
|
+
<span
|
|
23
|
+
key={logo}
|
|
24
|
+
className="text-[1.1875rem] font-semibold tracking-[-0.01em] text-muted-foreground"
|
|
25
|
+
>
|
|
23
26
|
{logo}
|
|
24
27
|
</span>
|
|
25
28
|
))}
|