@lukoweb/apitogo-module-landing 0.1.54 → 0.1.56
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 +40 -74
- package/src/landing.css +288 -0
- package/src/sections/ApiShowcaseSection.tsx +70 -0
- package/src/sections/CodeExampleCard.tsx +37 -0
- package/src/sections/FeaturesSection.tsx +53 -0
- package/src/sections/HeroSection.tsx +122 -0
- package/src/sections/LandingButtons.tsx +47 -0
- package/src/sections/LandingIcon.tsx +54 -0
- package/src/sections/PricingCtaSection.tsx +42 -0
- package/src/sections/TickerBar.tsx +45 -0
- package/src/sections/TrustSection.tsx +29 -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.56",
|
|
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.56",
|
|
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 { TrustSection } from "./sections/TrustSection.js";
|
|
9
|
+
import "./landing.css";
|
|
6
10
|
|
|
7
11
|
export type DefaultLandingPageProps = {
|
|
8
12
|
content: ResolvedLandingContent;
|
|
@@ -13,90 +17,52 @@ 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
|
-
const title = hero
|
|
18
|
-
const
|
|
19
|
-
const description =
|
|
20
|
-
hero?.description ??
|
|
21
|
-
options.metadata?.description ??
|
|
22
|
-
"Beautiful API documentation and developer portals powered by APIToGo.";
|
|
23
|
-
|
|
24
|
-
const features = content.features ?? [];
|
|
25
|
-
const primaryAction = content.primaryAction;
|
|
26
|
-
const secondaryAction = content.secondaryAction;
|
|
20
|
+
const hero = content.hero ?? { title: siteTitle };
|
|
21
|
+
const title = hero.title ?? siteTitle;
|
|
22
|
+
const description = hero.description;
|
|
27
23
|
|
|
28
24
|
return (
|
|
29
|
-
<div className="relative overflow-hidden">
|
|
25
|
+
<div className="apitogo-landing relative overflow-hidden">
|
|
30
26
|
<Head>
|
|
27
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
28
|
+
<link
|
|
29
|
+
rel="preconnect"
|
|
30
|
+
href="https://fonts.gstatic.com"
|
|
31
|
+
crossOrigin=""
|
|
32
|
+
/>
|
|
33
|
+
<link
|
|
34
|
+
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"
|
|
35
|
+
rel="stylesheet"
|
|
36
|
+
/>
|
|
31
37
|
<title>{title}</title>
|
|
32
38
|
{description ? <meta name="description" content={description} /> : null}
|
|
33
39
|
</Head>
|
|
34
40
|
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
<HeroSection
|
|
42
|
+
hero={{ ...hero, title }}
|
|
43
|
+
primaryAction={content.primaryAction}
|
|
44
|
+
secondaryAction={content.secondaryAction}
|
|
45
|
+
ticker={content.ticker}
|
|
38
46
|
/>
|
|
39
47
|
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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>
|
|
48
|
+
{content.trust ? <TrustSection trust={content.trust} /> : null}
|
|
49
|
+
{content.features ? (
|
|
50
|
+
<FeaturesSection
|
|
51
|
+
features={content.features}
|
|
52
|
+
section={content.featuresSection}
|
|
53
|
+
/>
|
|
54
|
+
) : null}
|
|
55
|
+
{content.apiShowcase ? (
|
|
56
|
+
<ApiShowcaseSection apiShowcase={content.apiShowcase} />
|
|
57
|
+
) : null}
|
|
58
|
+
{content.pricingCta ? (
|
|
59
|
+
<PricingCtaSection pricingCta={content.pricingCta} />
|
|
60
|
+
) : null}
|
|
95
61
|
|
|
96
62
|
{content.showPoweredBy !== false ? (
|
|
97
63
|
<div
|
|
98
64
|
className={cn(
|
|
99
|
-
"
|
|
65
|
+
"landing-powered-by py-6 text-center text-xs",
|
|
100
66
|
!options.site?.footer && "mb-8",
|
|
101
67
|
)}
|
|
102
68
|
>
|
package/src/landing.css
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
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
|
+
@keyframes landing-fu {
|
|
51
|
+
from {
|
|
52
|
+
opacity: 0;
|
|
53
|
+
transform: translateY(10px);
|
|
54
|
+
}
|
|
55
|
+
to {
|
|
56
|
+
opacity: 1;
|
|
57
|
+
transform: none;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@keyframes landing-ticker {
|
|
62
|
+
from {
|
|
63
|
+
transform: translateX(0);
|
|
64
|
+
}
|
|
65
|
+
to {
|
|
66
|
+
transform: translateX(-50%);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.apitogo-landing .landing-display {
|
|
71
|
+
font-family: var(--landing-display);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.apitogo-landing .landing-mono {
|
|
75
|
+
font-family: var(--landing-mono);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.apitogo-landing .landing-fu {
|
|
79
|
+
animation: landing-fu 0.5s cubic-bezier(0.2, 0.7, 0.3, 1) both;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.apitogo-landing .landing-section-label {
|
|
83
|
+
color: var(--landing-accent);
|
|
84
|
+
font-family: var(--landing-mono);
|
|
85
|
+
font-size: 0.8125rem;
|
|
86
|
+
font-weight: 600;
|
|
87
|
+
margin-bottom: 0.75rem;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.apitogo-landing .landing-muted {
|
|
91
|
+
color: var(--landing-text-2);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.apitogo-landing .landing-muted-3 {
|
|
95
|
+
color: var(--landing-text-3);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.apitogo-landing .landing-btn-primary {
|
|
99
|
+
align-items: center;
|
|
100
|
+
background: var(--landing-accent);
|
|
101
|
+
border: none;
|
|
102
|
+
border-radius: 11px;
|
|
103
|
+
box-shadow: 0 8px 24px -8px var(--landing-accent-soft);
|
|
104
|
+
color: var(--landing-on-accent);
|
|
105
|
+
cursor: pointer;
|
|
106
|
+
display: inline-flex;
|
|
107
|
+
font-family: inherit;
|
|
108
|
+
font-size: 0.96875rem;
|
|
109
|
+
font-weight: 600;
|
|
110
|
+
gap: 0.5rem;
|
|
111
|
+
padding: 0.8125rem 1.375rem;
|
|
112
|
+
text-decoration: none;
|
|
113
|
+
transition: background 0.15s ease;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.apitogo-landing .landing-btn-primary:hover {
|
|
117
|
+
background: var(--landing-accent-hover);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.apitogo-landing .landing-btn-secondary {
|
|
121
|
+
align-items: center;
|
|
122
|
+
background: var(--landing-surface);
|
|
123
|
+
border: 1px solid var(--landing-border);
|
|
124
|
+
border-radius: 11px;
|
|
125
|
+
color: var(--landing-text);
|
|
126
|
+
cursor: pointer;
|
|
127
|
+
display: inline-flex;
|
|
128
|
+
font-family: inherit;
|
|
129
|
+
font-size: 0.96875rem;
|
|
130
|
+
font-weight: 600;
|
|
131
|
+
gap: 0.5rem;
|
|
132
|
+
padding: 0.8125rem 1.375rem;
|
|
133
|
+
text-decoration: none;
|
|
134
|
+
transition:
|
|
135
|
+
border-color 0.15s ease,
|
|
136
|
+
background 0.15s ease;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.apitogo-landing .landing-btn-secondary:hover {
|
|
140
|
+
border-color: var(--landing-border-2);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.apitogo-landing .landing-btn-sm {
|
|
144
|
+
border-radius: 9px;
|
|
145
|
+
font-size: 0.875rem;
|
|
146
|
+
padding: 0.625rem 1rem;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.apitogo-landing .landing-hero-gradient {
|
|
150
|
+
background: radial-gradient(
|
|
151
|
+
1000px 480px at 78% -8%,
|
|
152
|
+
var(--landing-accent-soft),
|
|
153
|
+
transparent 70%
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.apitogo-landing .landing-badge {
|
|
158
|
+
align-items: center;
|
|
159
|
+
background: var(--landing-surface);
|
|
160
|
+
border: 1px solid var(--landing-border);
|
|
161
|
+
border-radius: 999px;
|
|
162
|
+
color: var(--landing-text-2);
|
|
163
|
+
display: inline-flex;
|
|
164
|
+
font-size: 0.78125rem;
|
|
165
|
+
gap: 0.5rem;
|
|
166
|
+
margin-bottom: 1.375rem;
|
|
167
|
+
padding: 0.375rem 0.75rem 0.375rem 0.5rem;
|
|
168
|
+
width: fit-content;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.apitogo-landing .landing-status-pill {
|
|
172
|
+
align-items: center;
|
|
173
|
+
background: var(--landing-green-soft);
|
|
174
|
+
border-radius: 999px;
|
|
175
|
+
color: var(--landing-green);
|
|
176
|
+
display: inline-flex;
|
|
177
|
+
font-family: var(--landing-mono);
|
|
178
|
+
font-size: 0.6875rem;
|
|
179
|
+
font-weight: 600;
|
|
180
|
+
gap: 0.3125rem;
|
|
181
|
+
padding: 0.125rem 0.5rem;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.apitogo-landing .landing-status-dot {
|
|
185
|
+
background: var(--landing-green);
|
|
186
|
+
border-radius: 50%;
|
|
187
|
+
height: 6px;
|
|
188
|
+
width: 6px;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.apitogo-landing .landing-feature-card {
|
|
192
|
+
background: var(--landing-surface);
|
|
193
|
+
border: 1px solid var(--landing-border);
|
|
194
|
+
border-radius: 14px;
|
|
195
|
+
padding: 1.5rem;
|
|
196
|
+
transition: border-color 0.15s ease;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.apitogo-landing .landing-feature-card:hover {
|
|
200
|
+
border-color: var(--landing-border-2);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.apitogo-landing .landing-feature-icon {
|
|
204
|
+
align-items: center;
|
|
205
|
+
background: var(--landing-accent-soft);
|
|
206
|
+
border-radius: 11px;
|
|
207
|
+
color: var(--landing-accent);
|
|
208
|
+
display: grid;
|
|
209
|
+
height: 2.625rem;
|
|
210
|
+
margin-bottom: 1rem;
|
|
211
|
+
place-items: center;
|
|
212
|
+
width: 2.625rem;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.apitogo-landing .landing-api-card {
|
|
216
|
+
background: var(--landing-surface);
|
|
217
|
+
border: 1px solid var(--landing-border);
|
|
218
|
+
border-radius: 13px;
|
|
219
|
+
color: var(--landing-text);
|
|
220
|
+
cursor: pointer;
|
|
221
|
+
padding: 1.125rem;
|
|
222
|
+
text-decoration: none;
|
|
223
|
+
transition:
|
|
224
|
+
border-color 0.15s ease,
|
|
225
|
+
transform 0.15s ease;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.apitogo-landing .landing-api-card:hover {
|
|
229
|
+
border-color: var(--landing-border-2);
|
|
230
|
+
transform: translateY(-2px);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.apitogo-landing .landing-api-icon {
|
|
234
|
+
align-items: center;
|
|
235
|
+
background: var(--landing-surface-2);
|
|
236
|
+
border: 1px solid var(--landing-border);
|
|
237
|
+
border-radius: 9px;
|
|
238
|
+
color: var(--landing-peri);
|
|
239
|
+
display: grid;
|
|
240
|
+
height: 2.25rem;
|
|
241
|
+
place-items: center;
|
|
242
|
+
width: 2.25rem;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.apitogo-landing .landing-latency-badge {
|
|
246
|
+
background: var(--landing-green-soft);
|
|
247
|
+
border-radius: 6px;
|
|
248
|
+
color: var(--landing-green);
|
|
249
|
+
font-family: var(--landing-mono);
|
|
250
|
+
font-size: 0.6875rem;
|
|
251
|
+
font-weight: 600;
|
|
252
|
+
padding: 0.1875rem 0.4375rem;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.apitogo-landing .landing-ticker-track {
|
|
256
|
+
animation: landing-ticker 34s linear infinite;
|
|
257
|
+
display: flex;
|
|
258
|
+
width: max-content;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.apitogo-landing .landing-ticker-shell {
|
|
262
|
+
background: var(--landing-bg-2);
|
|
263
|
+
border-top: 1px solid var(--landing-border);
|
|
264
|
+
overflow: hidden;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.apitogo-landing .landing-trust-logo {
|
|
268
|
+
color: var(--landing-text-2);
|
|
269
|
+
font-family: var(--landing-display);
|
|
270
|
+
font-size: 1.1875rem;
|
|
271
|
+
font-weight: 600;
|
|
272
|
+
letter-spacing: -0.01em;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.apitogo-landing .landing-showcase-section {
|
|
276
|
+
background: var(--landing-bg-2);
|
|
277
|
+
border-bottom: 1px solid var(--landing-border);
|
|
278
|
+
border-top: 1px solid var(--landing-border);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.apitogo-landing .landing-hero-section {
|
|
282
|
+
border-bottom: 1px solid var(--landing-border);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.apitogo-landing .landing-powered-by {
|
|
286
|
+
border-top: 1px solid var(--landing-border);
|
|
287
|
+
color: var(--landing-text-3);
|
|
288
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
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="landing-showcase-section">
|
|
23
|
+
<div className="mx-auto max-w-[1200px] px-6 py-[3.75rem]">
|
|
24
|
+
<div className="mb-8 flex flex-wrap items-end justify-between gap-5">
|
|
25
|
+
<div>
|
|
26
|
+
<p className="landing-section-label">// the catalog</p>
|
|
27
|
+
<h2 className="landing-display text-[2.125rem] leading-tight font-bold tracking-[-0.025em]">
|
|
28
|
+
{apiShowcase.title ?? "Production APIs. One key."}
|
|
29
|
+
</h2>
|
|
30
|
+
{apiShowcase.subtitle ? (
|
|
31
|
+
<p className="landing-muted mt-2">{apiShowcase.subtitle}</p>
|
|
32
|
+
) : null}
|
|
33
|
+
</div>
|
|
34
|
+
<Link
|
|
35
|
+
to={browseHref}
|
|
36
|
+
className="landing-btn-secondary landing-btn-sm"
|
|
37
|
+
>
|
|
38
|
+
{browseLabel}
|
|
39
|
+
<ArrowRightIcon size={15} aria-hidden />
|
|
40
|
+
</Link>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div className="grid gap-3.5 sm:grid-cols-2 lg:grid-cols-4">
|
|
44
|
+
{items.map((item) => (
|
|
45
|
+
<Link
|
|
46
|
+
key={`${item.name}-${item.path}`}
|
|
47
|
+
to={browseHref}
|
|
48
|
+
className="landing-api-card block"
|
|
49
|
+
>
|
|
50
|
+
<div className="mb-3.5 flex items-center justify-between">
|
|
51
|
+
<div className="landing-api-icon">
|
|
52
|
+
<LandingIcon name={item.icon ?? "boxes"} size={18} />
|
|
53
|
+
</div>
|
|
54
|
+
{item.latency ? (
|
|
55
|
+
<span className="landing-latency-badge">{item.latency}</span>
|
|
56
|
+
) : null}
|
|
57
|
+
</div>
|
|
58
|
+
<h3 className="landing-display mb-1 text-[0.9375rem] font-semibold">
|
|
59
|
+
{item.name}
|
|
60
|
+
</h3>
|
|
61
|
+
<p className="landing-mono landing-muted-3 text-[0.71875rem]">
|
|
62
|
+
{item.path}
|
|
63
|
+
</p>
|
|
64
|
+
</Link>
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</section>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
@@ -0,0 +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
|
|
16
|
+
className="overflow-hidden rounded-2xl border border-[var(--landing-border)] shadow-[var(--landing-shadow)]"
|
|
17
|
+
style={{ background: "var(--landing-code-bg)" }}
|
|
18
|
+
>
|
|
19
|
+
<div className="flex items-center gap-2 border-b border-[#1b1f2a] px-4 py-3">
|
|
20
|
+
<span className="size-[11px] rounded-full bg-[#ff5f57]" aria-hidden />
|
|
21
|
+
<span className="size-[11px] rounded-full bg-[#febc2e]" aria-hidden />
|
|
22
|
+
<span className="size-[11px] rounded-full bg-[#28c840]" aria-hidden />
|
|
23
|
+
<span className="landing-mono ms-2 text-xs text-[#6b7385]">
|
|
24
|
+
{filename}
|
|
25
|
+
</span>
|
|
26
|
+
<TerminalIcon
|
|
27
|
+
size={14}
|
|
28
|
+
className="ms-auto text-[#4b5162]"
|
|
29
|
+
aria-hidden
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
<pre className="landing-mono overflow-x-auto p-[1.125rem] text-[0.78125rem] leading-[1.85] text-[#cdd3e0]">
|
|
33
|
+
<code data-language={language}>{example.code}</code>
|
|
34
|
+
</pre>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
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 =
|
|
19
|
+
section?.title ?? "Everything you need to build on your API";
|
|
20
|
+
const description =
|
|
21
|
+
section?.description ??
|
|
22
|
+
"Skip the infra. Focus on your product while your developer portal handles docs, auth, and billing.";
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<section className="mx-auto max-w-[1200px] px-6 py-10 pb-16">
|
|
26
|
+
<div className="mb-11 max-w-[640px]">
|
|
27
|
+
<p className="landing-section-label">{label}</p>
|
|
28
|
+
<h2 className="landing-display mb-3.5 text-[2.375rem] leading-tight font-bold tracking-[-0.025em] text-balance">
|
|
29
|
+
{title}
|
|
30
|
+
</h2>
|
|
31
|
+
<p className="landing-muted text-[1.0625rem] leading-relaxed">
|
|
32
|
+
{description}
|
|
33
|
+
</p>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
37
|
+
{features.map((feature) => (
|
|
38
|
+
<article key={feature.title} className="landing-feature-card">
|
|
39
|
+
<div className="landing-feature-icon">
|
|
40
|
+
<LandingIcon name={feature.icon} size={21} />
|
|
41
|
+
</div>
|
|
42
|
+
<h3 className="landing-display mb-2 text-[1.09375rem] font-semibold">
|
|
43
|
+
{feature.title}
|
|
44
|
+
</h3>
|
|
45
|
+
<p className="landing-muted text-[0.90625rem] leading-relaxed">
|
|
46
|
+
{feature.description}
|
|
47
|
+
</p>
|
|
48
|
+
</article>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
</section>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
2
|
+
import { cn } from "@lukoweb/apitogo";
|
|
3
|
+
import { ArrowRightIcon, BookOpenIcon, CheckIcon } from "lucide-react";
|
|
4
|
+
import { CodeExampleCard } from "./CodeExampleCard.js";
|
|
5
|
+
import { LandingButton } from "./LandingButtons.js";
|
|
6
|
+
import { TickerBar } from "./TickerBar.js";
|
|
7
|
+
|
|
8
|
+
export type HeroSectionProps = {
|
|
9
|
+
hero: NonNullable<ResolvedLandingContent["hero"]>;
|
|
10
|
+
primaryAction?: ResolvedLandingContent["primaryAction"];
|
|
11
|
+
secondaryAction?: ResolvedLandingContent["secondaryAction"];
|
|
12
|
+
ticker?: ResolvedLandingContent["ticker"];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const HeroSection = ({
|
|
16
|
+
hero,
|
|
17
|
+
primaryAction,
|
|
18
|
+
secondaryAction,
|
|
19
|
+
ticker,
|
|
20
|
+
}: HeroSectionProps) => {
|
|
21
|
+
const hasCodeExample = Boolean(hero.codeExample?.code);
|
|
22
|
+
const highlights = hero.highlights ?? [];
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<section className="landing-hero-section relative overflow-hidden">
|
|
26
|
+
<div
|
|
27
|
+
aria-hidden
|
|
28
|
+
className="landing-hero-gradient pointer-events-none absolute inset-0"
|
|
29
|
+
/>
|
|
30
|
+
|
|
31
|
+
<div
|
|
32
|
+
className={
|
|
33
|
+
hasCodeExample
|
|
34
|
+
? "relative mx-auto grid max-w-[1200px] items-center gap-10 px-6 py-16 lg:grid-cols-[1.05fr_0.95fr] lg:gap-14 lg:px-6 lg:py-[4.75rem] lg:pb-16"
|
|
35
|
+
: "relative mx-auto flex max-w-[1200px] flex-col gap-10 px-6 py-16 lg:px-6 lg:py-24"
|
|
36
|
+
}
|
|
37
|
+
>
|
|
38
|
+
<div
|
|
39
|
+
className={cn(
|
|
40
|
+
"landing-fu flex flex-col",
|
|
41
|
+
!hasCodeExample && "mx-auto max-w-3xl items-center text-center",
|
|
42
|
+
)}
|
|
43
|
+
>
|
|
44
|
+
{(hero.badge || hero.subtitle) && (
|
|
45
|
+
<div className="landing-badge">
|
|
46
|
+
{hero.badge ? (
|
|
47
|
+
<span className="landing-status-pill">
|
|
48
|
+
<span className="landing-status-dot" aria-hidden />
|
|
49
|
+
{hero.badge}
|
|
50
|
+
</span>
|
|
51
|
+
) : null}
|
|
52
|
+
{hero.subtitle ? <span>{hero.subtitle}</span> : null}
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
<h1 className="landing-display mb-5 text-[2.5rem] leading-[1.02] font-bold tracking-[-0.03em] text-balance sm:text-5xl lg:text-[3.5rem]">
|
|
57
|
+
{hero.title}
|
|
58
|
+
{hero.titleAccent ? (
|
|
59
|
+
<>
|
|
60
|
+
<br />
|
|
61
|
+
<span className="text-[var(--landing-accent)]">
|
|
62
|
+
{hero.titleAccent}
|
|
63
|
+
</span>
|
|
64
|
+
</>
|
|
65
|
+
) : null}
|
|
66
|
+
</h1>
|
|
67
|
+
|
|
68
|
+
{hero.description ? (
|
|
69
|
+
<p className="landing-muted mb-[1.875rem] max-w-[480px] text-lg leading-relaxed text-pretty">
|
|
70
|
+
{hero.description}
|
|
71
|
+
</p>
|
|
72
|
+
) : null}
|
|
73
|
+
|
|
74
|
+
{(primaryAction || secondaryAction) && (
|
|
75
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
76
|
+
{primaryAction ? (
|
|
77
|
+
<LandingButton
|
|
78
|
+
href={primaryAction.href}
|
|
79
|
+
label={primaryAction.label}
|
|
80
|
+
icon={ArrowRightIcon}
|
|
81
|
+
variant="primary"
|
|
82
|
+
/>
|
|
83
|
+
) : null}
|
|
84
|
+
{secondaryAction ? (
|
|
85
|
+
<LandingButton
|
|
86
|
+
href={secondaryAction.href}
|
|
87
|
+
label={secondaryAction.label}
|
|
88
|
+
icon={BookOpenIcon}
|
|
89
|
+
iconPosition="start"
|
|
90
|
+
variant="secondary"
|
|
91
|
+
/>
|
|
92
|
+
) : null}
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
{highlights.length > 0 ? (
|
|
97
|
+
<ul className="landing-muted-3 mt-[1.875rem] flex flex-wrap items-center gap-x-[1.375rem] gap-y-2 text-[0.8125rem]">
|
|
98
|
+
{highlights.map((item) => (
|
|
99
|
+
<li key={item} className="inline-flex items-center gap-[0.4375rem]">
|
|
100
|
+
<CheckIcon
|
|
101
|
+
size={15}
|
|
102
|
+
className="text-[var(--landing-green)]"
|
|
103
|
+
aria-hidden
|
|
104
|
+
/>
|
|
105
|
+
{item}
|
|
106
|
+
</li>
|
|
107
|
+
))}
|
|
108
|
+
</ul>
|
|
109
|
+
) : null}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{hasCodeExample && hero.codeExample ? (
|
|
113
|
+
<div className="landing-fu">
|
|
114
|
+
<CodeExampleCard example={hero.codeExample} />
|
|
115
|
+
</div>
|
|
116
|
+
) : null}
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{ticker ? <TickerBar ticker={ticker} embedded /> : null}
|
|
120
|
+
</section>
|
|
121
|
+
);
|
|
122
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Link } from "@lukoweb/apitogo/components";
|
|
2
|
+
import { cn } from "@lukoweb/apitogo";
|
|
3
|
+
import type { LucideIcon } from "lucide-react";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
|
|
6
|
+
type LandingButtonProps = {
|
|
7
|
+
href: string;
|
|
8
|
+
label: string;
|
|
9
|
+
icon?: LucideIcon;
|
|
10
|
+
iconPosition?: "start" | "end";
|
|
11
|
+
variant?: "primary" | "secondary";
|
|
12
|
+
size?: "default" | "sm";
|
|
13
|
+
className?: string;
|
|
14
|
+
children?: ReactNode;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const LandingButton = ({
|
|
18
|
+
href,
|
|
19
|
+
label,
|
|
20
|
+
icon: Icon,
|
|
21
|
+
iconPosition = "end",
|
|
22
|
+
variant = "primary",
|
|
23
|
+
size = "default",
|
|
24
|
+
className,
|
|
25
|
+
children,
|
|
26
|
+
}: LandingButtonProps) => {
|
|
27
|
+
const content = children ?? (
|
|
28
|
+
<>
|
|
29
|
+
{Icon && iconPosition === "start" ? <Icon size={17} aria-hidden /> : null}
|
|
30
|
+
{label}
|
|
31
|
+
{Icon && iconPosition === "end" ? <Icon size={17} aria-hidden /> : null}
|
|
32
|
+
</>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Link
|
|
37
|
+
to={href}
|
|
38
|
+
className={cn(
|
|
39
|
+
variant === "primary" ? "landing-btn-primary" : "landing-btn-secondary",
|
|
40
|
+
size === "sm" && "landing-btn-sm",
|
|
41
|
+
className,
|
|
42
|
+
)}
|
|
43
|
+
>
|
|
44
|
+
{content}
|
|
45
|
+
</Link>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { LucideIcon } from "lucide-react";
|
|
2
|
+
import {
|
|
3
|
+
ActivityIcon,
|
|
4
|
+
ArrowRightLeftIcon,
|
|
5
|
+
BadgeDollarSignIcon,
|
|
6
|
+
BlocksIcon,
|
|
7
|
+
BookOpenIcon,
|
|
8
|
+
BoxesIcon,
|
|
9
|
+
CheckIcon,
|
|
10
|
+
CodeIcon,
|
|
11
|
+
CoinsIcon,
|
|
12
|
+
GaugeIcon,
|
|
13
|
+
LayersIcon,
|
|
14
|
+
RadioIcon,
|
|
15
|
+
ShieldCheckIcon,
|
|
16
|
+
WalletIcon,
|
|
17
|
+
WebhookIcon,
|
|
18
|
+
ZapIcon,
|
|
19
|
+
} from "lucide-react";
|
|
20
|
+
|
|
21
|
+
const ICONS: Record<string, LucideIcon> = {
|
|
22
|
+
activity: ActivityIcon,
|
|
23
|
+
"arrow-left-right": ArrowRightLeftIcon,
|
|
24
|
+
"arrow-right-left": ArrowRightLeftIcon,
|
|
25
|
+
"badge-dollar-sign": BadgeDollarSignIcon,
|
|
26
|
+
blocks: BlocksIcon,
|
|
27
|
+
"book-open": BookOpenIcon,
|
|
28
|
+
boxes: BoxesIcon,
|
|
29
|
+
check: CheckIcon,
|
|
30
|
+
code: CodeIcon,
|
|
31
|
+
coins: CoinsIcon,
|
|
32
|
+
gauge: GaugeIcon,
|
|
33
|
+
layers: LayersIcon,
|
|
34
|
+
radio: RadioIcon,
|
|
35
|
+
"shield-check": ShieldCheckIcon,
|
|
36
|
+
wallet: WalletIcon,
|
|
37
|
+
webhook: WebhookIcon,
|
|
38
|
+
zap: ZapIcon,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type LandingIconProps = {
|
|
42
|
+
name?: string;
|
|
43
|
+
size?: number;
|
|
44
|
+
className?: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const LandingIcon = ({
|
|
48
|
+
name,
|
|
49
|
+
size = 20,
|
|
50
|
+
className,
|
|
51
|
+
}: LandingIconProps) => {
|
|
52
|
+
const Icon = (name && ICONS[name]) || CodeIcon;
|
|
53
|
+
return <Icon size={size} className={className} aria-hidden />;
|
|
54
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
2
|
+
import { ArrowRightIcon } from "lucide-react";
|
|
3
|
+
import { LandingButton } from "./LandingButtons.js";
|
|
4
|
+
|
|
5
|
+
export type PricingCtaSectionProps = {
|
|
6
|
+
pricingCta: NonNullable<ResolvedLandingContent["pricingCta"]>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const PricingCtaSection = ({ pricingCta }: PricingCtaSectionProps) => {
|
|
10
|
+
const title = pricingCta.title ?? "Start free. Scale when you do.";
|
|
11
|
+
const description =
|
|
12
|
+
pricingCta.description ??
|
|
13
|
+
"Generous free tier. Upgrade only when your app takes off.";
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<section className="mx-auto max-w-[1200px] px-6 py-[4.5rem] text-center">
|
|
17
|
+
<h2 className="landing-display mb-4 text-[2.5rem] leading-tight font-bold tracking-[-0.03em] text-balance">
|
|
18
|
+
{title}
|
|
19
|
+
</h2>
|
|
20
|
+
<p className="landing-muted mx-auto mb-[1.875rem] max-w-[520px] text-[1.09375rem] leading-relaxed">
|
|
21
|
+
{description}
|
|
22
|
+
</p>
|
|
23
|
+
<div className="flex flex-wrap items-center justify-center gap-3">
|
|
24
|
+
{pricingCta.primaryAction ? (
|
|
25
|
+
<LandingButton
|
|
26
|
+
href={pricingCta.primaryAction.href}
|
|
27
|
+
label={pricingCta.primaryAction.label}
|
|
28
|
+
icon={ArrowRightIcon}
|
|
29
|
+
variant="primary"
|
|
30
|
+
/>
|
|
31
|
+
) : null}
|
|
32
|
+
{pricingCta.secondaryAction ? (
|
|
33
|
+
<LandingButton
|
|
34
|
+
href={pricingCta.secondaryAction.href}
|
|
35
|
+
label={pricingCta.secondaryAction.label}
|
|
36
|
+
variant="secondary"
|
|
37
|
+
/>
|
|
38
|
+
) : null}
|
|
39
|
+
</div>
|
|
40
|
+
</section>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ResolvedLandingContent } from "@lukoweb/apitogo";
|
|
2
|
+
|
|
3
|
+
export type TickerBarProps = {
|
|
4
|
+
ticker: NonNullable<ResolvedLandingContent["ticker"]>;
|
|
5
|
+
embedded?: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const TickerBar = ({ ticker, embedded = false }: TickerBarProps) => {
|
|
9
|
+
const items = ticker.items ?? [];
|
|
10
|
+
if (items.length === 0) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const loop = [...items, ...items];
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className={embedded ? "landing-ticker-shell" : "landing-ticker-shell border-t-0"}>
|
|
18
|
+
<div className="landing-ticker-track">
|
|
19
|
+
{loop.map((item, index) => (
|
|
20
|
+
<div
|
|
21
|
+
key={`${item.label}-${index}`}
|
|
22
|
+
className="landing-mono flex items-center gap-2.5 border-r border-[var(--landing-border)] px-[1.625rem] py-[0.8125rem] text-[0.8125rem] whitespace-nowrap"
|
|
23
|
+
>
|
|
24
|
+
<span className="landing-muted-3 font-semibold">{item.label}</span>
|
|
25
|
+
<span className="font-semibold text-[var(--landing-text)]">
|
|
26
|
+
{item.value}
|
|
27
|
+
</span>
|
|
28
|
+
{item.change ? (
|
|
29
|
+
<span
|
|
30
|
+
className="font-semibold"
|
|
31
|
+
style={{
|
|
32
|
+
color: item.change.trim().startsWith("-")
|
|
33
|
+
? "var(--landing-accent)"
|
|
34
|
+
: "var(--landing-green)",
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
{item.change}
|
|
38
|
+
</span>
|
|
39
|
+
) : null}
|
|
40
|
+
</div>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
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-[1200px] px-6 py-10">
|
|
15
|
+
{trust.title ? (
|
|
16
|
+
<p className="landing-muted-3 mb-[1.375rem] text-center text-[0.78125rem] font-semibold tracking-[0.08em] uppercase">
|
|
17
|
+
{trust.title}
|
|
18
|
+
</p>
|
|
19
|
+
) : null}
|
|
20
|
+
<div className="flex flex-wrap items-center justify-center gap-x-[2.875rem] gap-y-3.5 opacity-70">
|
|
21
|
+
{logos.map((logo) => (
|
|
22
|
+
<span key={logo} className="landing-trust-logo">
|
|
23
|
+
{logo}
|
|
24
|
+
</span>
|
|
25
|
+
))}
|
|
26
|
+
</div>
|
|
27
|
+
</section>
|
|
28
|
+
);
|
|
29
|
+
};
|