Files
365devnet/src/components/widgets/ModernCertifications.astro
Richard Bergsma d9a1b04f89 Implement touch handling for modals in Certifications, Education, and Skills components
- Introduced touch event listeners to enhance mobile interactivity for certification, education, and skills cards.
- Refactored modal display logic to improve user experience by allowing smooth scrolling and restoring scroll position after modal closure.
- Updated event handling to prevent default actions and ensure modals function correctly across devices.
2025-06-15 19:23:38 +02:00

365 lines
14 KiB
Plaintext

---
import Headline from '~/components/ui/Headline.astro';
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
import Button from '~/components/ui/Button.astro';
import Image from '~/components/common/Image.astro';
import { Icon } from 'astro-icon/components';
import type { Testimonials as Props } from '~/types';
// Import certification images
import SN_Logo2 from '~/assets/images/certificates/SN_Logo2.webp';
import CertifiedNexthinkProfessionalinApplicationExperienceManagement from '~/assets/images/certificates/CertifiedNexthinkProfessionalinApplicationExperienceManagement.webp';
import NexthinkAdministrator from '~/assets/images/certificates/NexthinkAdministrator.webp';
import NexthinkAssociate from '~/assets/images/certificates/NexthinkAssociate.webp';
import CrucialConversations_FMD_logo from '~/assets/images/certificates/CrucialConversations_FMD-logo.webp';
import PCEP from '~/assets/images/certificates/PCEP.webp';
import MicrosoftCertifiedAssociateBadge from '~/assets/images/certificates/microsoft-certified-associate-badge.webp';
import MicrosoftCertifiedFundamentalsBadge from '~/assets/images/certificates/microsoft-certified-fundamentals-badge.webp';
const {
title = '',
subtitle = '',
tagline = '',
testimonials = [],
callToAction,
id,
isDark = false,
classes = {},
bg = await Astro.slots.render('bg'),
} = Astro.props;
// Function to get the correct certification icon based on title
const getCertificationIcon = (name: string): string => {
const nameLower = name.toLowerCase();
if (nameLower.includes('stakeholder')) return 'tabler:users';
if (nameLower.includes('nexthink professional')) return 'tabler:chart-bar';
if (nameLower.includes('nexthink administrator')) return 'tabler:settings';
if (nameLower.includes('nexthink associate')) return 'tabler:certificate';
if (nameLower.includes('crucial conversations')) return 'tabler:message-circle';
if (nameLower.includes('python')) return 'tabler:brand-python';
if (nameLower.includes('desktop administrator')) return 'tabler:device-desktop';
if (nameLower.includes('microsoft 365')) return 'tabler:brand-office';
if (nameLower.includes('teams')) return 'tabler:brand-teams';
if (nameLower.includes('azure')) return 'tabler:cloud';
return 'tabler:award';
};
// Function to get the correct gradient colors based on certification
const getCertificationGradient = (name: string): string => {
const nameLower = name.toLowerCase();
if (nameLower.includes('stakeholder')) return 'from-blue-500 to-purple-600';
if (nameLower.includes('nexthink')) return 'from-green-500 to-teal-600';
if (nameLower.includes('crucial')) return 'from-orange-500 to-red-500';
if (nameLower.includes('python')) return 'from-yellow-500 to-green-500';
if (nameLower.includes('microsoft')) return 'from-blue-600 to-indigo-600';
if (nameLower.includes('azure')) return 'from-blue-400 to-cyan-500';
return 'from-gray-500 to-gray-600';
};
// Function to get the correct image source
const getImageSrc = (imagePath: string) => {
switch (imagePath) {
case '/images/certificates/SN_Logo2.webp':
return SN_Logo2;
case '/images/certificates/CertifiedNexthinkProfessionalinApplicationExperienceManagement.webp':
return CertifiedNexthinkProfessionalinApplicationExperienceManagement;
case '/images/certificates/NexthinkAdministrator.webp':
return NexthinkAdministrator;
case '/images/certificates/NexthinkAssociate.webp':
return NexthinkAssociate;
case '/images/certificates/CrucialConversations_FMD-logo.webp':
return CrucialConversations_FMD_logo;
case '/images/certificates/PCEP.webp':
return PCEP;
case '/images/certificates/microsoft-certified-associate-badge.webp':
return MicrosoftCertifiedAssociateBadge;
case '/images/certificates/microsoft-certified-fundamentals-badge.webp':
return MicrosoftCertifiedFundamentalsBadge;
default:
return imagePath;
}
};
---
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-7xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
<Headline title={title} subtitle={subtitle} tagline={tagline} classes={{
container: 'max-w-3xl',
title: 'text-3xl lg:text-4xl',
}} />
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 mt-8">
{
testimonials &&
testimonials.map(({ linkUrl, name, issueDate, description, image }) => {
const imageSrc = getImageSrc(image?.src || '');
const icon = getCertificationIcon(name || '');
const gradient = getCertificationGradient(name || '');
return (
<div
class="cert-card bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm rounded-2xl p-5 transition-all duration-300 cursor-pointer border border-gray-100 dark:border-slate-800 hover:transform hover:scale-105 hover:shadow-xl relative overflow-hidden group"
data-cert-title={name}
data-cert-date={issueDate}
data-cert-description={description}
data-cert-url={linkUrl}
>
<!-- Top gradient bar -->
<div class={`absolute top-0 left-0 right-0 h-1 bg-gradient-to-r ${gradient}`}></div>
<!-- Certification Icon -->
<div class={`w-12 h-12 rounded-xl bg-gradient-to-r ${gradient} flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300`}>
<Icon name={icon} class="w-6 h-6 text-white" />
</div>
<!-- Badge Image -->
{imageSrc && (
<div class="w-16 h-16 mx-auto mb-4 rounded-lg overflow-hidden bg-gray-50 dark:bg-gray-800 flex items-center justify-center group-hover:scale-105 transition-transform duration-300">
<Image
src={imageSrc}
alt={name || "Certification badge"}
class="w-full h-full object-contain"
/>
</div>
)}
<!-- Content -->
<div class="text-center">
<h3 class="font-semibold text-sm text-gray-900 dark:text-white mb-2 line-clamp-2 group-hover:text-transparent group-hover:bg-clip-text group-hover:bg-gradient-to-r group-hover:from-blue-600 group-hover:to-purple-600 transition-all duration-300">
{name}
</h3>
<p class={`text-xs font-medium text-transparent bg-clip-text bg-gradient-to-r ${gradient}`}>
{issueDate}
</p>
</div>
<!-- Hover effect overlay -->
<div class="absolute inset-0 bg-gradient-to-r from-blue-500/5 to-purple-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-2xl"></div>
<!-- Shimmer effect -->
<div class="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-700 ease-out"></div>
</div>
);
})
}
</div>
{
callToAction && (
<div class="flex justify-center mx-auto w-fit mt-8 font-medium">
<Button {...callToAction} />
</div>
)
}
</WidgetWrapper>
<!-- Certification Details Modal -->
<div id="certModal" class="modal fixed inset-0 bg-black/50 backdrop-blur-sm z-50 hidden items-center justify-center p-4">
<div class="modal-content bg-white dark:bg-slate-800 rounded-2xl p-6 max-w-2xl w-full mx-4 max-h-[80vh] overflow-y-auto transform scale-90 opacity-0 transition-all duration-300">
<div class="flex justify-between items-start mb-4">
<div>
<h3 id="certModalTitle" class="text-2xl font-bold text-gray-900 dark:text-white mb-1"></h3>
<p id="certModalDate" class="text-sm text-blue-600 dark:text-blue-400 font-medium"></p>
</div>
<button onclick="closeCertModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<Icon name="tabler:x" class="w-6 h-6" />
</button>
</div>
<p id="certModalDescription" class="text-gray-700 dark:text-gray-300 leading-relaxed mb-6"></p>
<div class="flex justify-end">
<a id="certModalLink" href="#" target="_blank" rel="noopener noreferrer" class="inline-flex items-center px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 text-white font-semibold rounded-xl hover:shadow-lg transition-all duration-300 hover:scale-105">
<Icon name="tabler:external-link" class="w-4 h-4 mr-2" />
View Certification
</a>
</div>
</div>
</div>
<style>
.cert-card {
animation: fadeInUp 0.6s ease-out forwards;
opacity: 0;
transform: translateY(20px);
}
.cert-card:nth-child(1) { animation-delay: 0.1s; }
.cert-card:nth-child(2) { animation-delay: 0.15s; }
.cert-card:nth-child(3) { animation-delay: 0.2s; }
.cert-card:nth-child(4) { animation-delay: 0.25s; }
.cert-card:nth-child(5) { animation-delay: 0.3s; }
.cert-card:nth-child(6) { animation-delay: 0.35s; }
.cert-card:nth-child(7) { animation-delay: 0.4s; }
.cert-card:nth-child(8) { animation-delay: 0.45s; }
.cert-card:nth-child(9) { animation-delay: 0.5s; }
.cert-card:nth-child(10) { animation-delay: 0.55s; }
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
.modal {
backdrop-filter: blur(10px);
}
.modal.active {
display: flex !important;
}
.modal.active .modal-content {
transform: scale(1);
opacity: 1;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
<script is:inline>
// Certifications widget touch handling - namespaced to avoid conflicts
(function() {
let certTouchStart = { x: 0, y: 0, time: 0 };
let certIsScrolling = false;
let certOriginalScrollPosition = 0;
let certClickedElement = null;
function certHandleTouchStart(e) {
const touch = e.touches[0];
certTouchStart = {
x: touch.clientX,
y: touch.clientY,
time: Date.now()
};
certIsScrolling = false;
}
function certHandleTouchMove(e) {
const touch = e.touches[0];
const deltaX = Math.abs(touch.clientX - certTouchStart.x);
const deltaY = Math.abs(touch.clientY - certTouchStart.y);
if (deltaX > 10 || deltaY > 10) {
certIsScrolling = true;
}
}
function certHandleTouchEnd(e, element) {
const touch = e.changedTouches[0];
const touchEndTime = Date.now();
if (certIsScrolling) return;
const touchDuration = touchEndTime - certTouchStart.time;
if (touchDuration > 300) return;
const deltaX = Math.abs(touch.clientX - certTouchStart.x);
const deltaY = Math.abs(touch.clientY - certTouchStart.y);
if (deltaX > 15 || deltaY > 15) return;
e.preventDefault();
showCertModal(element);
}
window.showCertModal = function(element) {
// Store the clicked element and current scroll position
certClickedElement = element;
certOriginalScrollPosition = window.pageYOffset || document.documentElement.scrollTop;
const title = element.dataset.certTitle;
const date = element.dataset.certDate;
const description = element.dataset.certDescription;
const url = element.dataset.certUrl;
document.getElementById('certModalTitle').textContent = title;
document.getElementById('certModalDate').textContent = date;
document.getElementById('certModalDescription').textContent = description;
document.getElementById('certModalLink').href = url;
const modal = document.getElementById('certModal');
modal.classList.add('active');
setTimeout(() => {
const modalContent = modal.querySelector('.modal-content');
if (modalContent) {
modalContent.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 100);
};
window.closeCertModal = function() {
const modal = document.getElementById('certModal');
modal.classList.remove('active');
// Restore scroll position to the original card
if (certClickedElement) {
setTimeout(() => {
// First, scroll to the original position
window.scrollTo({
top: certOriginalScrollPosition,
behavior: 'smooth'
});
// Then, ensure the clicked card is visible with a slight offset
setTimeout(() => {
const elementRect = certClickedElement.getBoundingClientRect();
const isElementVisible = elementRect.top >= 0 && elementRect.bottom <= window.innerHeight;
if (!isElementVisible) {
certClickedElement.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
}, 300);
}, 100);
}
// Clear the reference
certClickedElement = null;
};
document.addEventListener('DOMContentLoaded', function() {
const certCards = document.querySelectorAll('.cert-card');
certCards.forEach(card => {
card.addEventListener('click', function(e) {
if (!('ontouchstart' in window)) {
e.preventDefault();
showCertModal(this);
}
});
card.addEventListener('touchstart', certHandleTouchStart, { passive: true });
card.addEventListener('touchmove', certHandleTouchMove, { passive: true });
card.addEventListener('touchend', function(e) {
certHandleTouchEnd(e, this);
}, { passive: false });
});
const certModal = document.getElementById('certModal');
if (certModal) {
certModal.addEventListener('click', function(e) {
if (e.target === this) {
closeCertModal();
}
});
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeCertModal();
}
});
});
})();
</script>