Skip to content

Commit 6bcfd0b

Browse files
authored
Merge pull request #173 from DefGuard/Customer-testimonials-section-on-frontpage
Customer testimonials section on frontpage
2 parents ac35558 + 602fa83 commit 6bcfd0b

File tree

6 files changed

+56
-186
lines changed

6 files changed

+56
-186
lines changed
42.5 KB
Loading
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
title: Dimitar D.
3+
subtitle: Director of IT Infrastructure
4+
---
5+
6+
Defguard may be a young product, but it already outperforms most commercial VPN solutions we evaluated. We migrated 300–400 employees from FortiGate and saw immediate gains in speed, security, and user experience, all at a lower cost. What stood out was the modern, open foundation built on WireGuard and Rust, plus seamless SSO enrollment through Google Workspace. Early onboarding UX had a few rough edges, but the team moved fast on our feedback, releasing updates that made adoption smooth. Today, it's a mature zero-trust compliant platform backed by a responsive team.

src/pages/_home/components/client-side/Testimonials/Testimonials.tsx

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -62,22 +62,26 @@ const Testimonial = ({ data }: TestimonialProps) => {
6262
<div className="testimonial">
6363
<div className="left">
6464
<div className="image-logo-wrapper">
65-
<div className="image">
66-
<img
67-
src={data.imagePrimary}
68-
alt="person image"
69-
loading="lazy"
70-
decoding="async"
71-
/>
72-
</div>
73-
<div className="logo">
74-
<img
75-
src={data.imageSecondary}
76-
alt="logo image"
77-
loading="lazy"
78-
decoding="async"
79-
/>
80-
</div>
65+
{data.imagePrimary && (
66+
<div className="image">
67+
<img
68+
src={data.imagePrimary}
69+
alt="person image"
70+
loading="lazy"
71+
decoding="async"
72+
/>
73+
</div>
74+
)}
75+
{data.imageSecondary && (
76+
<div className="logo">
77+
<img
78+
src={data.imageSecondary}
79+
alt="logo image"
80+
loading="lazy"
81+
decoding="async"
82+
/>
83+
</div>
84+
)}
8185
</div>
8286
</div>
8387
<div className="right">

src/pages/_home/components/client-side/Testimonials/type.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { z } from "zod";
33
export const testimonialSchema = z.object({
44
title: z.string().min(1),
55
subtitle: z.string().optional(),
6-
imagePrimary: z.string().min(1),
7-
imageSecondary: z.string().min(1),
6+
imagePrimary: z.string().min(1).optional(),
7+
imageSecondary: z.string().min(1).optional(),
88
markdownRaw: z.string().optional(),
99
});
1010

src/pages/_home/components/client-side/TestimonialsGrid/TestimonialsGrid.astro

Lines changed: 20 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,25 @@ const { data } = Astro.props;
1717
<div class="quote-icon">
1818
"
1919
</div>
20-
<div class="secondary-image">
21-
<img
22-
src={testimonial.imageSecondary}
23-
alt="logo image"
24-
loading="lazy"
25-
decoding="async"
26-
/>
27-
</div>
20+
{testimonial.imageSecondary && (
21+
<div class="secondary-image">
22+
<img
23+
src={testimonial.imageSecondary}
24+
alt="logo image"
25+
loading="lazy"
26+
decoding="async"
27+
/>
28+
</div>
29+
)}
2830
</div>
2931
<div class="content-wrapper">
30-
<div class="testimonial-content" data-testimonial-content>
32+
<div class="testimonial-content">
3133
{testimonial.markdownRaw ? (
3234
<ClientMarkdown data={testimonial.markdownRaw} client:load />
3335
) : (
3436
<p>No content available</p>
3537
)}
3638
</div>
37-
<button class="read-more-btn" data-read-more-btn style="display: none;">
38-
Read More
39-
</button>
4039
</div>
4140
<div class="testimonial-author">
4241
<div class="author-name">{testimonial.title}</div>
@@ -52,29 +51,23 @@ const { data } = Astro.props;
5251
<style is:global>
5352
.testimonials-grid ul {
5453
display: grid;
55-
grid-template-columns: repeat(3, 1fr);
54+
grid-template-columns: repeat(2, 1fr);
5655
gap: 1.5rem;
5756
padding: 2rem 0;
5857
list-style: none;
5958
margin: 0;
6059
justify-items: center;
6160
}
6261

63-
/* Center single item in second row when there are 4 testimonials */
64-
.testimonials-grid li:nth-child(4):nth-last-child(1) {
65-
grid-column: 2;
66-
}
67-
6862
.testimonials-grid li {
69-
height: 320px; /* forced height */
63+
min-height: 280px;
7064
padding: 1.25rem;
7165
border: 1px solid var(--border);
7266
border-radius: 8px;
7367
background: var(--surface-nav-bg);
7468
display: flex;
7569
flex-direction: column;
7670
align-items: flex-start;
77-
transition: height 0.4s cubic-bezier(0.4, 0, 0.2, 1);
7871
}
7972

8073
.testimonial-header {
@@ -116,44 +109,13 @@ const { data } = Astro.props;
116109
}
117110

118111
.testimonial-content {
119-
max-height: 9rem; /* fixed height for paragraph space */
120-
overflow: hidden;
121112
margin-bottom: 1rem;
122-
transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1),
123-
mask-image 0.2s cubic-bezier(0.4, 0, 0.2, 1),
124-
-webkit-mask-image 0.2s cubic-bezier(0.4, 0, 0.2, 1);
125-
position: relative;
126-
/* No default mask - will be applied by JavaScript only when needed */
127-
}
128-
129-
.testimonial-content.expanded {
130-
max-height: 100rem;
131-
display: block;
132-
mask-image: none;
133-
-webkit-mask-image: none;
134-
}
135-
136-
.read-more-btn {
137-
@include typography(paragraph, var(--text-body-primary));
138-
background: none;
139-
border: none;
140-
cursor: pointer;
141-
padding: 0;
142-
margin: 0;
143-
text-decoration: underline;
144-
align-self: flex-end; /* position on the right */
145-
& {
146-
font-size: calc(0.95rem * var(--font-scale-factor));
147-
}
148-
}
149-
150-
.read-more-btn:hover {
151-
opacity: 0.8;
152113
}
153114

154115
.testimonial-author {
155116
margin-top: auto;
156117
padding-bottom: 0;
118+
text-align: left;
157119
}
158120

159121
.author-name {
@@ -172,55 +134,31 @@ const { data } = Astro.props;
172134
}
173135
}
174136

175-
/* Style for content within testimonial-content */
137+
/* Style for content within testimonial-content - all text left-aligned */
138+
.testimonial-content {
139+
text-align: left;
140+
}
141+
176142
.testimonial-content p {
177143
@include typography(paragraph);
178144
margin: 0;
179-
text-align: justify;
145+
text-align: left;
180146
& {
181147
font-size: calc(0.95rem * var(--font-scale-factor));
182148
}
183149
}
184150

185151
/* Media queries for responsive design */
186-
/* Tablet: 2 columns (below 992px) */
187-
@media (max-width: 991.98px) {
188-
.testimonials-grid ul {
189-
grid-template-columns: repeat(2, 1fr) !important;
190-
}
191-
192-
/* Reset centering for 2-column layout */
193-
.testimonials-grid li:nth-child(4):nth-last-child(1) {
194-
grid-column: auto;
195-
}
196-
197-
/* Adjust text clamping for 2-column layout */
198-
.testimonial-content {
199-
max-height: 10.5rem; /* slightly more height for 2 columns */
200-
}
201-
}
202-
203152
/* Mobile: 1 column (below 768px) */
204153
@media (max-width: 767.98px) {
205154
.testimonials-grid ul {
206155
grid-template-columns: 1fr !important;
207156
gap: 1rem;
208157
}
209158

210-
/* Reset centering for 1-column layout */
211-
.testimonials-grid li:nth-child(4):nth-last-child(1) {
212-
grid-column: auto;
213-
}
214-
215159
.testimonials-grid li {
216-
height: auto;
217160
min-height: 280px;
218161
}
219-
220-
/* Adjust text clamping for 1-column layout */
221-
.testimonial-content {
222-
max-height: 8rem; /* adjust height for mobile */
223-
}
224162
}
225163

226164
/* Small Mobile: optimized spacing (below 576px) */
@@ -240,89 +178,3 @@ const { data } = Astro.props;
240178
}
241179
}
242180
</style>
243-
244-
<script>
245-
// Wait for DOM to be fully loaded
246-
document.addEventListener('DOMContentLoaded', function() {
247-
// Function to check if content is overflowing and show/hide read more button
248-
function checkOverflow() {
249-
const testimonialCards = document.querySelectorAll('.testimonials-grid li');
250-
251-
testimonialCards.forEach(card => {
252-
const content = card.querySelector('[data-testimonial-content]') as HTMLElement;
253-
const readMoreBtn = card.querySelector('[data-read-more-btn]') as HTMLElement;
254-
255-
if (content && readMoreBtn) {
256-
// Check if content is taller than its container
257-
const isOverflowing = content.scrollHeight > content.clientHeight;
258-
259-
if (isOverflowing) {
260-
readMoreBtn.style.display = 'block';
261-
// Apply gradient mask only when content overflows
262-
content.style.maskImage = 'linear-gradient(to bottom, black 80%, transparent 100%)';
263-
(content.style as any).webkitMaskImage = 'linear-gradient(to bottom, black 80%, transparent 100%)';
264-
} else {
265-
readMoreBtn.style.display = 'none';
266-
// Remove gradient mask when content doesn't overflow
267-
content.style.maskImage = 'none';
268-
(content.style as any).webkitMaskImage = 'none';
269-
}
270-
}
271-
});
272-
}
273-
274-
// Function to handle read more button clicks
275-
function handleReadMore() {
276-
const readMoreButtons = document.querySelectorAll('[data-read-more-btn]');
277-
278-
readMoreButtons.forEach(button => {
279-
button.addEventListener('click', function(this: HTMLButtonElement) {
280-
const card = this.closest('li') as HTMLElement;
281-
const content = card.querySelector('[data-testimonial-content]') as HTMLElement;
282-
283-
if (content.classList.contains('expanded')) {
284-
// Collapse - control animation entirely through JavaScript
285-
this.textContent = 'Read More';
286-
287-
// First set explicit max-height to current scroll height
288-
content.style.maxHeight = content.scrollHeight + 'px';
289-
290-
// Force reflow
291-
content.offsetHeight;
292-
293-
// Then animate to collapsed height
294-
setTimeout(() => {
295-
content.style.maxHeight = '9rem';
296-
content.style.maskImage = 'linear-gradient(to bottom, black 80%, transparent 100%)';
297-
(content.style as any).webkitMaskImage = 'linear-gradient(to bottom, black 80%, transparent 100%)';
298-
}, 10);
299-
300-
// Remove expanded class after animation
301-
setTimeout(() => {
302-
content.classList.remove('expanded');
303-
card.style.height = '320px';
304-
}, 400);
305-
306-
} else {
307-
// Expand - remove mask and expand
308-
content.style.maskImage = 'none';
309-
(content.style as any).webkitMaskImage = 'none';
310-
content.classList.add('expanded');
311-
312-
// Set max-height to scroll height for smooth animation
313-
content.style.maxHeight = content.scrollHeight + 'px';
314-
card.style.height = 'auto';
315-
this.textContent = 'Read Less';
316-
}
317-
});
318-
});
319-
}
320-
321-
// Initialize
322-
checkOverflow();
323-
handleReadMore();
324-
325-
// Recheck on window resize
326-
window.addEventListener('resize', checkOverflow);
327-
});
328-
</script>

src/pages/index.astro

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ const testimonialsData: Array<TestimonialData> = testimonialsImportData.map((val
3131
return res;
3232
});
3333
34+
// Swap placement of Dimitar D. and Jan Zajc (Sipro) testimonials
35+
const enterpriseIdx = testimonialsData.findIndex((t) => t.title === "Dimitar D.");
36+
const siproIdx = testimonialsData.findIndex((t) => t.title === "Jan Zajc");
37+
if (enterpriseIdx !== -1 && siproIdx !== -1) {
38+
[testimonialsData[enterpriseIdx], testimonialsData[siproIdx]] =
39+
[testimonialsData[siproIdx], testimonialsData[enterpriseIdx]];
40+
}
41+
3442
const title = "defguard - Zero-Trust WireGuard® 2FA/MFA VPN";
3543
const featuredImage =
3644
"github.com/DefGuard/defguard.github.io/raw/main/public/images/product/core/hero-image.png";

0 commit comments

Comments
 (0)