Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/images/data/testimonials/dext-by-iris.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/data/home/testimonials/enterprise-fortigate-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: Dimitar D.
subtitle: Director of IT Infrastructure
---

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.
Original file line number Diff line number Diff line change
Expand Up @@ -62,22 +62,26 @@ const Testimonial = ({ data }: TestimonialProps) => {
<div className="testimonial">
<div className="left">
<div className="image-logo-wrapper">
<div className="image">
<img
src={data.imagePrimary}
alt="person image"
loading="lazy"
decoding="async"
/>
</div>
<div className="logo">
<img
src={data.imageSecondary}
alt="logo image"
loading="lazy"
decoding="async"
/>
</div>
{data.imagePrimary && (
<div className="image">
<img
src={data.imagePrimary}
alt="person image"
loading="lazy"
decoding="async"
/>
</div>
)}
{data.imageSecondary && (
<div className="logo">
<img
src={data.imageSecondary}
alt="logo image"
loading="lazy"
decoding="async"
/>
</div>
)}
</div>
</div>
<div className="right">
Expand Down
4 changes: 2 additions & 2 deletions src/pages/_home/components/client-side/Testimonials/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { z } from "zod";
export const testimonialSchema = z.object({
title: z.string().min(1),
subtitle: z.string().optional(),
imagePrimary: z.string().min(1),
imageSecondary: z.string().min(1),
imagePrimary: z.string().min(1).optional(),
imageSecondary: z.string().min(1).optional(),
markdownRaw: z.string().optional(),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,25 @@ const { data } = Astro.props;
<div class="quote-icon">
"
</div>
<div class="secondary-image">
<img
src={testimonial.imageSecondary}
alt="logo image"
loading="lazy"
decoding="async"
/>
</div>
{testimonial.imageSecondary && (
<div class="secondary-image">
<img
src={testimonial.imageSecondary}
alt="logo image"
loading="lazy"
decoding="async"
/>
</div>
)}
</div>
<div class="content-wrapper">
<div class="testimonial-content" data-testimonial-content>
<div class="testimonial-content">
{testimonial.markdownRaw ? (
<ClientMarkdown data={testimonial.markdownRaw} client:load />
) : (
<p>No content available</p>
)}
</div>
<button class="read-more-btn" data-read-more-btn style="display: none;">
Read More
</button>
</div>
<div class="testimonial-author">
<div class="author-name">{testimonial.title}</div>
Expand All @@ -52,29 +51,23 @@ const { data } = Astro.props;
<style is:global>
.testimonials-grid ul {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
padding: 2rem 0;
list-style: none;
margin: 0;
justify-items: center;
}

/* Center single item in second row when there are 4 testimonials */
.testimonials-grid li:nth-child(4):nth-last-child(1) {
grid-column: 2;
}

.testimonials-grid li {
height: 320px; /* forced height */
min-height: 280px;
padding: 1.25rem;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface-nav-bg);
display: flex;
flex-direction: column;
align-items: flex-start;
transition: height 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}

.testimonial-header {
Expand Down Expand Up @@ -116,44 +109,13 @@ const { data } = Astro.props;
}

.testimonial-content {
max-height: 9rem; /* fixed height for paragraph space */
overflow: hidden;
margin-bottom: 1rem;
transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1),
mask-image 0.2s cubic-bezier(0.4, 0, 0.2, 1),
-webkit-mask-image 0.2s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
/* No default mask - will be applied by JavaScript only when needed */
}

.testimonial-content.expanded {
max-height: 100rem;
display: block;
mask-image: none;
-webkit-mask-image: none;
}

.read-more-btn {
@include typography(paragraph, var(--text-body-primary));
background: none;
border: none;
cursor: pointer;
padding: 0;
margin: 0;
text-decoration: underline;
align-self: flex-end; /* position on the right */
& {
font-size: calc(0.95rem * var(--font-scale-factor));
}
}

.read-more-btn:hover {
opacity: 0.8;
}

.testimonial-author {
margin-top: auto;
padding-bottom: 0;
text-align: left;
}

.author-name {
Expand All @@ -172,55 +134,31 @@ const { data } = Astro.props;
}
}

/* Style for content within testimonial-content */
/* Style for content within testimonial-content - all text left-aligned */
.testimonial-content {
text-align: left;
}

.testimonial-content p {
@include typography(paragraph);
margin: 0;
text-align: justify;
text-align: left;
& {
font-size: calc(0.95rem * var(--font-scale-factor));
}
}

/* Media queries for responsive design */
/* Tablet: 2 columns (below 992px) */
@media (max-width: 991.98px) {
.testimonials-grid ul {
grid-template-columns: repeat(2, 1fr) !important;
}

/* Reset centering for 2-column layout */
.testimonials-grid li:nth-child(4):nth-last-child(1) {
grid-column: auto;
}

/* Adjust text clamping for 2-column layout */
.testimonial-content {
max-height: 10.5rem; /* slightly more height for 2 columns */
}
}

/* Mobile: 1 column (below 768px) */
@media (max-width: 767.98px) {
.testimonials-grid ul {
grid-template-columns: 1fr !important;
gap: 1rem;
}

/* Reset centering for 1-column layout */
.testimonials-grid li:nth-child(4):nth-last-child(1) {
grid-column: auto;
}

.testimonials-grid li {
height: auto;
min-height: 280px;
}

/* Adjust text clamping for 1-column layout */
.testimonial-content {
max-height: 8rem; /* adjust height for mobile */
}
}

/* Small Mobile: optimized spacing (below 576px) */
Expand All @@ -240,89 +178,3 @@ const { data } = Astro.props;
}
}
</style>

<script>
// Wait for DOM to be fully loaded
document.addEventListener('DOMContentLoaded', function() {
// Function to check if content is overflowing and show/hide read more button
function checkOverflow() {
const testimonialCards = document.querySelectorAll('.testimonials-grid li');

testimonialCards.forEach(card => {
const content = card.querySelector('[data-testimonial-content]') as HTMLElement;
const readMoreBtn = card.querySelector('[data-read-more-btn]') as HTMLElement;

if (content && readMoreBtn) {
// Check if content is taller than its container
const isOverflowing = content.scrollHeight > content.clientHeight;

if (isOverflowing) {
readMoreBtn.style.display = 'block';
// Apply gradient mask only when content overflows
content.style.maskImage = 'linear-gradient(to bottom, black 80%, transparent 100%)';
(content.style as any).webkitMaskImage = 'linear-gradient(to bottom, black 80%, transparent 100%)';
} else {
readMoreBtn.style.display = 'none';
// Remove gradient mask when content doesn't overflow
content.style.maskImage = 'none';
(content.style as any).webkitMaskImage = 'none';
}
}
});
}

// Function to handle read more button clicks
function handleReadMore() {
const readMoreButtons = document.querySelectorAll('[data-read-more-btn]');

readMoreButtons.forEach(button => {
button.addEventListener('click', function(this: HTMLButtonElement) {
const card = this.closest('li') as HTMLElement;
const content = card.querySelector('[data-testimonial-content]') as HTMLElement;

if (content.classList.contains('expanded')) {
// Collapse - control animation entirely through JavaScript
this.textContent = 'Read More';

// First set explicit max-height to current scroll height
content.style.maxHeight = content.scrollHeight + 'px';

// Force reflow
content.offsetHeight;

// Then animate to collapsed height
setTimeout(() => {
content.style.maxHeight = '9rem';
content.style.maskImage = 'linear-gradient(to bottom, black 80%, transparent 100%)';
(content.style as any).webkitMaskImage = 'linear-gradient(to bottom, black 80%, transparent 100%)';
}, 10);

// Remove expanded class after animation
setTimeout(() => {
content.classList.remove('expanded');
card.style.height = '320px';
}, 400);

} else {
// Expand - remove mask and expand
content.style.maskImage = 'none';
(content.style as any).webkitMaskImage = 'none';
content.classList.add('expanded');

// Set max-height to scroll height for smooth animation
content.style.maxHeight = content.scrollHeight + 'px';
card.style.height = 'auto';
this.textContent = 'Read Less';
}
});
});
}

// Initialize
checkOverflow();
handleReadMore();

// Recheck on window resize
window.addEventListener('resize', checkOverflow);
});
</script>
8 changes: 8 additions & 0 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ const testimonialsData: Array<TestimonialData> = testimonialsImportData.map((val
return res;
});

// Swap placement of Dimitar D. and Jan Zajc (Sipro) testimonials
const enterpriseIdx = testimonialsData.findIndex((t) => t.title === "Dimitar D.");
const siproIdx = testimonialsData.findIndex((t) => t.title === "Jan Zajc");
if (enterpriseIdx !== -1 && siproIdx !== -1) {
[testimonialsData[enterpriseIdx], testimonialsData[siproIdx]] =
[testimonialsData[siproIdx], testimonialsData[enterpriseIdx]];
}

const title = "defguard - Zero-Trust WireGuard® 2FA/MFA VPN";
const featuredImage =
"github.com/DefGuard/defguard.github.io/raw/main/public/images/product/core/hero-image.png";
Expand Down