@ugurdemirel/landcraft 0.1.2 → 0.1.4
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/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +41 -22
- package/dist/index.js +2261 -1937
- package/dist/index.js.map +1 -1
- package/dist/styles.css +3 -0
- package/package.json +1 -1
- package/src/components/Blog/BlogCard.tsx +12 -0
- package/src/components/Blog/BlogSection.tsx +95 -0
- package/src/components/Blog/index.ts +8 -0
- package/src/components/Blog/parts.tsx +38 -0
- package/src/components/Blog/types.ts +42 -0
- package/src/components/Blog/variants/Card.tsx +62 -0
- package/src/components/Blog/variants/Row.tsx +41 -0
- package/src/components/CTA/CTA.tsx +12 -0
- package/src/components/CTA/index.ts +2 -0
- package/src/components/CTA/parts.tsx +26 -0
- package/src/components/CTA/types.ts +12 -0
- package/src/components/CTA/variants/Inverse.tsx +50 -0
- package/src/components/CTA/variants/Panel.tsx +60 -0
- package/src/components/CTA/variants/Surface.tsx +34 -0
- package/src/components/FAQ/FAQ.tsx +48 -0
- package/src/components/FAQ/index.ts +2 -0
- package/src/components/FAQ/parts.tsx +74 -0
- package/src/components/FAQ/types.ts +18 -0
- package/src/components/FAQ/variants/Accordion.tsx +46 -0
- package/src/components/FAQ/variants/Cards.tsx +40 -0
- package/src/components/FAQ/variants/Split.tsx +44 -0
- package/src/components/Features/FeatureCard.tsx +44 -0
- package/src/components/Features/FeatureGrid.tsx +14 -0
- package/src/components/Features/index.ts +3 -0
- package/src/components/Features/parts.ts +15 -0
- package/src/components/Features/types.ts +25 -0
- package/src/components/Features/variants/Bento.tsx +32 -0
- package/src/components/Features/variants/Columns.tsx +21 -0
- package/src/components/Features/variants/EditorialRows.tsx +38 -0
- package/src/components/Footer/Footer.tsx +11 -0
- package/src/components/Footer/index.ts +2 -0
- package/src/components/Footer/parts.tsx +101 -0
- package/src/components/Footer/types.ts +32 -0
- package/src/components/Footer/variants/Classic.tsx +59 -0
- package/src/components/Footer/variants/Editorial.tsx +47 -0
- package/src/components/Footer/variants/Minimal.tsx +33 -0
- package/src/components/Hero/Hero.tsx +12 -0
- package/src/components/Hero/index.ts +2 -0
- package/src/components/Hero/parts.tsx +29 -0
- package/src/components/Hero/types.ts +20 -0
- package/src/components/Hero/variants/Centered.tsx +54 -0
- package/src/components/Hero/variants/Split.tsx +53 -0
- package/src/components/Hero/variants/Statement.tsx +60 -0
- package/src/components/LanguageSwitcher/LanguageSwitcher.tsx +37 -0
- package/src/components/LanguageSwitcher/index.ts +6 -0
- package/src/components/LanguageSwitcher/parts.tsx +144 -0
- package/src/components/LanguageSwitcher/types.ts +40 -0
- package/src/components/LanguageSwitcher/useLanguageSwitcher.ts +23 -0
- package/src/components/LanguageSwitcher/variants/Dropdown.tsx +88 -0
- package/src/components/LanguageSwitcher/variants/Modal.tsx +56 -0
- package/src/components/LogoCloud/LogoCloud.tsx +14 -0
- package/src/components/LogoCloud/index.ts +2 -0
- package/src/components/LogoCloud/parts.tsx +32 -0
- package/src/components/LogoCloud/types.ts +16 -0
- package/src/components/LogoCloud/variants/Marquee.tsx +30 -0
- package/src/components/LogoCloud/variants/Quiet.tsx +26 -0
- package/src/components/LogoCloud/variants/Strip.tsx +25 -0
- package/src/components/{MegaMenu.tsx → MegaMenu/MegaMenu.tsx} +108 -126
- package/src/components/MegaMenu/index.ts +9 -0
- package/src/components/MegaMenu/types.ts +56 -0
- package/src/components/Modal.tsx +1 -0
- package/src/components/Navbar/Navbar.tsx +185 -0
- package/src/components/Navbar/index.ts +2 -0
- package/src/components/Navbar/types.ts +30 -0
- package/src/components/Newsletter/Newsletter.tsx +12 -0
- package/src/components/Newsletter/index.ts +2 -0
- package/src/components/Newsletter/parts.tsx +23 -0
- package/src/components/Newsletter/types.ts +12 -0
- package/src/components/Newsletter/useNewsletterForm.ts +27 -0
- package/src/components/Newsletter/variants/Card.tsx +62 -0
- package/src/components/Newsletter/variants/Inline.tsx +54 -0
- package/src/components/Newsletter/variants/Underline.tsx +52 -0
- package/src/components/Pricing/Pricing.tsx +37 -0
- package/src/components/Pricing/index.ts +2 -0
- package/src/components/Pricing/parts.tsx +132 -0
- package/src/components/Pricing/types.ts +28 -0
- package/src/components/Pricing/variants/Bento.tsx +72 -0
- package/src/components/Pricing/variants/Cards.tsx +79 -0
- package/src/components/Pricing/variants/Compact.tsx +69 -0
- package/src/components/Stats/Stats.tsx +16 -0
- package/src/components/Stats/index.ts +2 -0
- package/src/components/Stats/parts.tsx +29 -0
- package/src/components/Stats/types.ts +23 -0
- package/src/components/Stats/variants/Cells.tsx +34 -0
- package/src/components/Stats/variants/Editorial.tsx +41 -0
- package/src/components/Stats/variants/Hairline.tsx +31 -0
- package/src/components/Stats/variants/Ticker.tsx +36 -0
- package/src/components/Testimonials/TestimonialCard.tsx +61 -0
- package/src/components/Testimonials/Testimonials.tsx +14 -0
- package/src/components/Testimonials/index.ts +3 -0
- package/src/components/Testimonials/parts.ts +7 -0
- package/src/components/Testimonials/types.ts +24 -0
- package/src/components/Testimonials/variants/Carousel.tsx +94 -0
- package/src/components/Testimonials/variants/Grid.tsx +19 -0
- package/src/components/Testimonials/variants/Marquee.tsx +28 -0
- package/src/utils/useTokenForeground.ts +1 -0
- package/src/components/Blog.tsx +0 -251
- package/src/components/CTA.tsx +0 -155
- package/src/components/FAQ.tsx +0 -163
- package/src/components/Features.tsx +0 -153
- package/src/components/Footer.tsx +0 -185
- package/src/components/Hero.tsx +0 -169
- package/src/components/LanguageSwitcher.tsx +0 -223
- package/src/components/LogoCloud.tsx +0 -100
- package/src/components/Navbar.tsx +0 -195
- package/src/components/Newsletter.tsx +0 -180
- package/src/components/Pricing.tsx +0 -336
- package/src/components/Stats.tsx +0 -181
- package/src/components/Testimonials.tsx +0 -205
package/src/components/Stats.tsx
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
|
|
2
|
-
import { cn } from "../utils/cn";
|
|
3
|
-
import { ArrowUp, ArrowDown } from "../icons";
|
|
4
|
-
|
|
5
|
-
export interface Stat {
|
|
6
|
-
value: string;
|
|
7
|
-
label: string;
|
|
8
|
-
/** Small unit rendered inline with the value, e.g. "ms" or "₺". */
|
|
9
|
-
suffix?: string;
|
|
10
|
-
/** Trend change in percent — renders an up/down badge. */
|
|
11
|
-
delta?: number;
|
|
12
|
-
/** Optional supporting copy. */
|
|
13
|
-
sub?: string;
|
|
14
|
-
icon?: ReactNode;
|
|
15
|
-
/** Highlights the value with the primary token. */
|
|
16
|
-
accent?: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface StatsProps extends HTMLAttributes<HTMLDListElement> {
|
|
20
|
-
stats: Stat[];
|
|
21
|
-
option?: "editorial" | "hairline" | "cells" | "ticker";
|
|
22
|
-
columns?: 2 | 3 | 4;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const valueBase =
|
|
26
|
-
"font-display font-semibold tabular-nums tracking-[-0.02em] text-foreground";
|
|
27
|
-
|
|
28
|
-
const suffixBase = "ml-1 font-sans text-sm font-normal tracking-normal text-muted-foreground";
|
|
29
|
-
|
|
30
|
-
const gridCols: Record<NonNullable<StatsProps["columns"]>, string> = {
|
|
31
|
-
2: "sm:grid-cols-2",
|
|
32
|
-
3: "sm:grid-cols-2 lg:grid-cols-3",
|
|
33
|
-
4: "sm:grid-cols-2 lg:grid-cols-4",
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
function DeltaBadge({ delta }: { delta: number }) {
|
|
37
|
-
const isUp = delta >= 0;
|
|
38
|
-
const Icon = isUp ? ArrowUp : ArrowDown;
|
|
39
|
-
return (
|
|
40
|
-
<span
|
|
41
|
-
className={cn(
|
|
42
|
-
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-semibold tabular-nums",
|
|
43
|
-
isUp ? "bg-accent/10 text-accent" : "bg-danger-soft text-danger",
|
|
44
|
-
)}
|
|
45
|
-
>
|
|
46
|
-
<Icon className="h-3 w-3" />
|
|
47
|
-
{Math.abs(delta)}%
|
|
48
|
-
</span>
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export const Stats = forwardRef<HTMLDListElement, StatsProps>(
|
|
53
|
-
({ className, stats, option = "editorial", columns = 4, ...props }, ref) => {
|
|
54
|
-
if (option === "ticker") {
|
|
55
|
-
return (
|
|
56
|
-
<dl
|
|
57
|
-
ref={ref}
|
|
58
|
-
className={cn(
|
|
59
|
-
"flex w-full items-stretch overflow-x-auto rounded-2xl border border-border bg-surface",
|
|
60
|
-
className,
|
|
61
|
-
)}
|
|
62
|
-
{...props}
|
|
63
|
-
>
|
|
64
|
-
{stats.map((stat, i) => (
|
|
65
|
-
<div
|
|
66
|
-
key={stat.label}
|
|
67
|
-
className={cn(
|
|
68
|
-
"flex min-w-[11rem] flex-1 flex-col gap-3 p-6",
|
|
69
|
-
i > 0 && "border-l border-border",
|
|
70
|
-
)}
|
|
71
|
-
>
|
|
72
|
-
<div className="flex items-center justify-between gap-3">
|
|
73
|
-
<dt className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
|
74
|
-
{stat.label}
|
|
75
|
-
</dt>
|
|
76
|
-
{typeof stat.delta === "number" ? <DeltaBadge delta={stat.delta} /> : null}
|
|
77
|
-
</div>
|
|
78
|
-
<dd className={cn(valueBase, "text-3xl", stat.accent ? "text-primary" : undefined)}>
|
|
79
|
-
{stat.value}
|
|
80
|
-
{stat.suffix ? <span className={suffixBase}>{stat.suffix}</span> : null}
|
|
81
|
-
</dd>
|
|
82
|
-
{stat.sub ? (
|
|
83
|
-
<p className="text-[13px] leading-5 text-muted-foreground">{stat.sub}</p>
|
|
84
|
-
) : null}
|
|
85
|
-
</div>
|
|
86
|
-
))}
|
|
87
|
-
</dl>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (option === "cells") {
|
|
92
|
-
return (
|
|
93
|
-
<dl
|
|
94
|
-
ref={ref}
|
|
95
|
-
className={cn("grid grid-cols-1 gap-4", gridCols[columns], className)}
|
|
96
|
-
{...props}
|
|
97
|
-
>
|
|
98
|
-
{stats.map((stat) => (
|
|
99
|
-
<div
|
|
100
|
-
key={stat.label}
|
|
101
|
-
className="flex min-h-[8.5rem] flex-col justify-between rounded-xl border border-border bg-surface p-6"
|
|
102
|
-
>
|
|
103
|
-
<dd className={cn(valueBase, "text-3xl", stat.accent ? "text-primary" : undefined)}>
|
|
104
|
-
{stat.value}
|
|
105
|
-
{stat.suffix ? <span className={suffixBase}>{stat.suffix}</span> : null}
|
|
106
|
-
</dd>
|
|
107
|
-
<div className="mt-5 flex items-center justify-between gap-3">
|
|
108
|
-
<dt className="text-sm text-muted-foreground">{stat.label}</dt>
|
|
109
|
-
{stat.icon ? (
|
|
110
|
-
<span className="text-foreground/30">{stat.icon}</span>
|
|
111
|
-
) : null}
|
|
112
|
-
</div>
|
|
113
|
-
</div>
|
|
114
|
-
))}
|
|
115
|
-
</dl>
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (option === "hairline") {
|
|
120
|
-
return (
|
|
121
|
-
<dl
|
|
122
|
-
ref={ref}
|
|
123
|
-
className={cn("grid grid-cols-1 gap-y-10", gridCols[columns], className)}
|
|
124
|
-
{...props}
|
|
125
|
-
>
|
|
126
|
-
{stats.map((stat, i) => (
|
|
127
|
-
<div
|
|
128
|
-
key={stat.label}
|
|
129
|
-
className={cn(
|
|
130
|
-
"text-center",
|
|
131
|
-
columns > 1 && i > 0 && "sm:border-l sm:border-border",
|
|
132
|
-
)}
|
|
133
|
-
>
|
|
134
|
-
<dd className={cn(valueBase, "text-5xl", stat.accent ? "text-primary" : undefined)}>
|
|
135
|
-
{stat.value}
|
|
136
|
-
{stat.suffix ? <span className={suffixBase}>{stat.suffix}</span> : null}
|
|
137
|
-
</dd>
|
|
138
|
-
<dt className="mt-3 text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
139
|
-
{stat.label}
|
|
140
|
-
</dt>
|
|
141
|
-
</div>
|
|
142
|
-
))}
|
|
143
|
-
</dl>
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// editorial — label above, oversized number, hairline rule under.
|
|
148
|
-
return (
|
|
149
|
-
<dl
|
|
150
|
-
ref={ref}
|
|
151
|
-
className={cn("grid grid-cols-1 gap-x-10 gap-y-12", gridCols[columns], className)}
|
|
152
|
-
{...props}
|
|
153
|
-
>
|
|
154
|
-
{stats.map((stat) => (
|
|
155
|
-
<div key={stat.label} className="border-t border-border pt-6">
|
|
156
|
-
<div className="flex items-center justify-between gap-3">
|
|
157
|
-
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
|
158
|
-
{stat.label}
|
|
159
|
-
</dt>
|
|
160
|
-
{typeof stat.delta === "number" ? <DeltaBadge delta={stat.delta} /> : null}
|
|
161
|
-
</div>
|
|
162
|
-
<dd
|
|
163
|
-
className={cn(
|
|
164
|
-
valueBase,
|
|
165
|
-
"mt-4 text-5xl sm:text-6xl",
|
|
166
|
-
stat.accent ? "text-primary" : undefined,
|
|
167
|
-
)}
|
|
168
|
-
>
|
|
169
|
-
{stat.value}
|
|
170
|
-
{stat.suffix ? <span className={suffixBase}>{stat.suffix}</span> : null}
|
|
171
|
-
</dd>
|
|
172
|
-
{stat.sub ? (
|
|
173
|
-
<p className="mt-3 max-w-xs text-[13px] leading-5 text-muted-foreground">{stat.sub}</p>
|
|
174
|
-
) : null}
|
|
175
|
-
</div>
|
|
176
|
-
))}
|
|
177
|
-
</dl>
|
|
178
|
-
);
|
|
179
|
-
},
|
|
180
|
-
);
|
|
181
|
-
Stats.displayName = "Stats";
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import { forwardRef, useState, type HTMLAttributes, type ReactNode } from "react";
|
|
2
|
-
import { cn } from "../utils/cn";
|
|
3
|
-
import { getContrastText } from "../utils/contrast";
|
|
4
|
-
import { ChevronLeft, ChevronRight, Quote, Star } from "../icons";
|
|
5
|
-
|
|
6
|
-
export interface Testimonial {
|
|
7
|
-
quote: string;
|
|
8
|
-
author: string;
|
|
9
|
-
role?: string;
|
|
10
|
-
company?: string;
|
|
11
|
-
avatar?: ReactNode;
|
|
12
|
-
/** Arbitrary CSS color for the initials avatar. */
|
|
13
|
-
accent?: string;
|
|
14
|
-
rating?: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface TestimonialCardProps extends HTMLAttributes<HTMLElement> {
|
|
18
|
-
testimonial: Testimonial;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const initials = (name: string) =>
|
|
22
|
-
name
|
|
23
|
-
.split(" ")
|
|
24
|
-
.map((p) => p[0])
|
|
25
|
-
.join("")
|
|
26
|
-
.slice(0, 2)
|
|
27
|
-
.toUpperCase();
|
|
28
|
-
|
|
29
|
-
export const TestimonialCard = forwardRef<HTMLElement, TestimonialCardProps>(
|
|
30
|
-
function TestimonialCard({ className, testimonial, ...props }, ref) {
|
|
31
|
-
const { quote, author, role, company, avatar, accent, rating } = testimonial;
|
|
32
|
-
const badgeStyle = accent
|
|
33
|
-
? { backgroundColor: accent, color: getContrastText(accent, "#111111", "#ffffff") }
|
|
34
|
-
: undefined;
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<figure
|
|
38
|
-
ref={ref}
|
|
39
|
-
className={cn(
|
|
40
|
-
"mb-4 flex flex-col break-inside-avoid rounded-2xl border border-border bg-surface p-7",
|
|
41
|
-
className,
|
|
42
|
-
)}
|
|
43
|
-
{...props}
|
|
44
|
-
>
|
|
45
|
-
<div className="mb-5 flex items-center justify-between">
|
|
46
|
-
<Quote className={cn("h-6 w-6", accent ? undefined : "text-primary/30")} style={accent ? { color: accent } : undefined} />
|
|
47
|
-
{rating ? (
|
|
48
|
-
<span className="flex items-center gap-0.5 text-amber-500" aria-label={`${rating} / 5`}>
|
|
49
|
-
{Array.from({ length: 5 }).map((_, i) => (
|
|
50
|
-
<Star key={i} className="h-3.5 w-3.5" />
|
|
51
|
-
))}
|
|
52
|
-
</span>
|
|
53
|
-
) : null}
|
|
54
|
-
</div>
|
|
55
|
-
<blockquote className="flex-1 text-[15px] leading-7 text-foreground/85">{quote}</blockquote>
|
|
56
|
-
<figcaption className="mt-6 flex items-center gap-3">
|
|
57
|
-
{avatar ? (
|
|
58
|
-
avatar
|
|
59
|
-
) : (
|
|
60
|
-
<span
|
|
61
|
-
style={badgeStyle}
|
|
62
|
-
className={cn(
|
|
63
|
-
"inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-xs font-bold",
|
|
64
|
-
!accent && "bg-primary-soft text-primary",
|
|
65
|
-
)}
|
|
66
|
-
>
|
|
67
|
-
{initials(author)}
|
|
68
|
-
</span>
|
|
69
|
-
)}
|
|
70
|
-
<div>
|
|
71
|
-
<div className="text-sm font-semibold text-foreground">{author}</div>
|
|
72
|
-
{(role || company) && (
|
|
73
|
-
<div className="text-xs text-muted-foreground">
|
|
74
|
-
{[role, company].filter(Boolean).join(" · ")}
|
|
75
|
-
</div>
|
|
76
|
-
)}
|
|
77
|
-
</div>
|
|
78
|
-
</figcaption>
|
|
79
|
-
</figure>
|
|
80
|
-
);
|
|
81
|
-
},
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
/** Holds per-option rendering + controls together. */
|
|
85
|
-
export interface TestimonialsProps extends HTMLAttributes<HTMLDivElement> {
|
|
86
|
-
items: Testimonial[];
|
|
87
|
-
option?: "grid" | "carousel" | "marquee";
|
|
88
|
-
title?: string;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export const Testimonials = ({ className, items, option = "grid", ...props }: TestimonialsProps) => {
|
|
92
|
-
if (option === "carousel") {
|
|
93
|
-
return <Carousel items={items} className={className} {...props} />;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (option === "marquee") {
|
|
97
|
-
const doubled = [...items, ...items];
|
|
98
|
-
return (
|
|
99
|
-
<div className={cn("w-full overflow-hidden", className)} {...props}>
|
|
100
|
-
<div className="mask-fade-x">
|
|
101
|
-
<div className="flex w-max animate-marquee gap-4 pr-4 motion-reduce:animate-none">
|
|
102
|
-
{doubled.map((item, i) => (
|
|
103
|
-
<div key={i} className="w-[19rem] shrink-0 sm:w-[24rem]">
|
|
104
|
-
<TestimonialCard testimonial={item} className="m-0" />
|
|
105
|
-
</div>
|
|
106
|
-
))}
|
|
107
|
-
</div>
|
|
108
|
-
</div>
|
|
109
|
-
</div>
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return (
|
|
114
|
-
<div className={cn("columns-1 gap-4 md:columns-2 lg:columns-3", className)} {...props}>
|
|
115
|
-
{items.map((item, i) => (
|
|
116
|
-
<TestimonialCard key={i} testimonial={item} />
|
|
117
|
-
))}
|
|
118
|
-
</div>
|
|
119
|
-
);
|
|
120
|
-
};
|
|
121
|
-
Testimonials.displayName = "Testimonials";
|
|
122
|
-
|
|
123
|
-
function Carousel({
|
|
124
|
-
items,
|
|
125
|
-
className,
|
|
126
|
-
...props
|
|
127
|
-
}: HTMLAttributes<HTMLDivElement> & { items: Testimonial[] }) {
|
|
128
|
-
const [index, setIndex] = useState(0);
|
|
129
|
-
const item = items[index % items.length];
|
|
130
|
-
const badgeStyle = item.accent
|
|
131
|
-
? { backgroundColor: item.accent, color: getContrastText(item.accent, "#111111", "#ffffff") }
|
|
132
|
-
: undefined;
|
|
133
|
-
|
|
134
|
-
const go = (dir: 1 | -1) => setIndex((i) => (i + dir + items.length) % items.length);
|
|
135
|
-
|
|
136
|
-
return (
|
|
137
|
-
<div className={cn("mx-auto w-full max-w-3xl", className)} {...props}>
|
|
138
|
-
<figure className="relative rounded-2xl border border-border bg-surface p-10 text-center sm:p-14">
|
|
139
|
-
<Quote
|
|
140
|
-
className={cn("mx-auto mb-6 h-8 w-8", item.accent ? undefined : "text-primary/25")}
|
|
141
|
-
style={item.accent ? { color: item.accent } : undefined}
|
|
142
|
-
aria-hidden
|
|
143
|
-
/>
|
|
144
|
-
<blockquote className="font-display text-balance text-xl font-medium leading-relaxed tracking-tight text-foreground sm:text-2xl">
|
|
145
|
-
{item.quote}
|
|
146
|
-
</blockquote>
|
|
147
|
-
<figcaption className="mt-8 flex items-center justify-center gap-3">
|
|
148
|
-
{item.avatar ? (
|
|
149
|
-
item.avatar
|
|
150
|
-
) : (
|
|
151
|
-
<span
|
|
152
|
-
style={badgeStyle}
|
|
153
|
-
className={cn(
|
|
154
|
-
"inline-flex h-11 w-11 items-center justify-center rounded-full text-xs font-bold",
|
|
155
|
-
!item.accent && "bg-foreground text-background",
|
|
156
|
-
)}
|
|
157
|
-
>
|
|
158
|
-
{initials(item.author)}
|
|
159
|
-
</span>
|
|
160
|
-
)}
|
|
161
|
-
<div className="text-left">
|
|
162
|
-
<div className="text-sm font-semibold text-foreground">{item.author}</div>
|
|
163
|
-
<div className="text-xs text-muted-foreground">
|
|
164
|
-
{[item.role, item.company].filter(Boolean).join(" · ")}
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
</figcaption>
|
|
168
|
-
</figure>
|
|
169
|
-
|
|
170
|
-
<div className="mt-6 flex items-center justify-center gap-4">
|
|
171
|
-
<button
|
|
172
|
-
type="button"
|
|
173
|
-
aria-label="Previous testimonial"
|
|
174
|
-
onClick={() => go(-1)}
|
|
175
|
-
className="inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-full border border-border bg-surface text-foreground transition-colors duration-150 hover:border-foreground/25 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
176
|
-
>
|
|
177
|
-
<ChevronLeft className="h-4 w-4" />
|
|
178
|
-
</button>
|
|
179
|
-
<div className="flex items-center gap-1.5">
|
|
180
|
-
{items.map((_, i) => (
|
|
181
|
-
<button
|
|
182
|
-
key={i}
|
|
183
|
-
type="button"
|
|
184
|
-
aria-label={`Testimonial ${i + 1}`}
|
|
185
|
-
aria-current={i === index}
|
|
186
|
-
onClick={() => setIndex(i)}
|
|
187
|
-
className={cn(
|
|
188
|
-
"h-1.5 cursor-pointer rounded-full transition-all duration-200",
|
|
189
|
-
i === index ? "w-6 bg-foreground" : "w-1.5 bg-foreground/25 hover:bg-foreground/50",
|
|
190
|
-
)}
|
|
191
|
-
/>
|
|
192
|
-
))}
|
|
193
|
-
</div>
|
|
194
|
-
<button
|
|
195
|
-
type="button"
|
|
196
|
-
aria-label="Next testimonial"
|
|
197
|
-
onClick={() => go(1)}
|
|
198
|
-
className="inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-full border border-border bg-surface text-foreground transition-colors duration-150 hover:border-foreground/25 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
199
|
-
>
|
|
200
|
-
<ChevronRight className="h-4 w-4" />
|
|
201
|
-
</button>
|
|
202
|
-
</div>
|
|
203
|
-
</div>
|
|
204
|
-
);
|
|
205
|
-
}
|