Enhance About Me page with modern components and animations
- Replaced existing widgets with modern versions for Work Experience, Certifications, Skills, and Education sections to improve visual appeal and interactivity. - Introduced an animated hero background and enhanced call-to-action buttons for better user engagement. - Implemented staggered animations for content sections to create a more dynamic user experience. - Updated styles for a cohesive glassmorphism effect across all sections, enhancing the overall aesthetic of the page.
This commit is contained in:
278
src/components/widgets/ModernCertifications.astro
Normal file
278
src/components/widgets/ModernCertifications.astro
Normal file
@@ -0,0 +1,278 @@
|
||||
---
|
||||
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 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>
|
||||
function showCertModal(element) {
|
||||
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');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeCertModal() {
|
||||
const modal = document.getElementById('certModal');
|
||||
modal.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// Initialize when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Add click listeners to cert cards
|
||||
const certCards = document.querySelectorAll('.cert-card');
|
||||
certCards.forEach(card => {
|
||||
card.addEventListener('click', function() {
|
||||
showCertModal(this);
|
||||
});
|
||||
});
|
||||
|
||||
// Close modal when clicking outside
|
||||
const certModal = document.getElementById('certModal');
|
||||
if (certModal) {
|
||||
certModal.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeCertModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close modal with Escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeCertModal();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
260
src/components/widgets/ModernEducation.astro
Normal file
260
src/components/widgets/ModernEducation.astro
Normal file
@@ -0,0 +1,260 @@
|
||||
---
|
||||
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||
import Headline from '~/components/ui/Headline.astro';
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import type { Steps as Props } from '~/types';
|
||||
|
||||
const {
|
||||
title = await Astro.slots.render('title'),
|
||||
subtitle = await Astro.slots.render('subtitle'),
|
||||
tagline = await Astro.slots.render('tagline'),
|
||||
items = [],
|
||||
|
||||
id,
|
||||
isDark = false,
|
||||
classes = {},
|
||||
bg = await Astro.slots.render('bg'),
|
||||
} = Astro.props;
|
||||
|
||||
// Function to get the appropriate icon and gradient based on education level
|
||||
const getEducationStyle = (title: string) => {
|
||||
const titleLower = title.toLowerCase();
|
||||
|
||||
if (titleLower.includes('bachelor')) {
|
||||
return {
|
||||
icon: 'tabler:school',
|
||||
gradient: 'from-blue-500 to-indigo-600',
|
||||
badgeIcon: 'tabler:book-2',
|
||||
completed: false
|
||||
};
|
||||
} else if (titleLower.includes('associate')) {
|
||||
return {
|
||||
icon: 'tabler:certificate',
|
||||
gradient: 'from-green-500 to-teal-600',
|
||||
badgeIcon: 'tabler:check',
|
||||
completed: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
icon: 'tabler:graduation',
|
||||
gradient: 'from-purple-500 to-pink-600',
|
||||
badgeIcon: 'tabler:check',
|
||||
completed: true
|
||||
};
|
||||
}
|
||||
};
|
||||
---
|
||||
|
||||
<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',
|
||||
...((classes?.headline as object) ?? {}),
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mt-8">
|
||||
{items.map((item, index) => {
|
||||
const educationStyle = getEducationStyle(item.title || '');
|
||||
|
||||
return (
|
||||
<div
|
||||
class="education-card group bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm rounded-2xl p-6 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"
|
||||
data-education-title={item.title || ''}
|
||||
data-education-description={item.description || ''}
|
||||
>
|
||||
<!-- Top gradient bar -->
|
||||
<div class={`absolute top-0 left-0 right-0 h-1 bg-gradient-to-r ${educationStyle.gradient}`}></div>
|
||||
|
||||
<!-- Education Icon -->
|
||||
<div class={`w-12 h-12 rounded-xl bg-gradient-to-r ${educationStyle.gradient} flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300 shadow-lg`}>
|
||||
<Icon name={educationStyle.icon} class="w-6 h-6 text-white" />
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="education-content">
|
||||
<div
|
||||
class="font-semibold text-sm leading-relaxed text-gray-900 dark:text-white 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"
|
||||
set:html={item.title}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Achievement Badge (bottom right) -->
|
||||
<div class={`absolute bottom-4 right-4 w-6 h-6 rounded-full bg-gradient-to-r ${educationStyle.gradient} flex items-center justify-center opacity-60 group-hover:opacity-100 transition-opacity duration-300`}>
|
||||
<Icon name={educationStyle.badgeIcon} class="w-3 h-3 text-white" />
|
||||
</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>
|
||||
</WidgetWrapper>
|
||||
|
||||
<!-- Education Details Modal -->
|
||||
<div id="educationModal" 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 transform scale-90 opacity-0 transition-all duration-300">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 id="educationModalTitle" class="text-2xl font-bold text-gray-900 dark:text-white mb-1"></h3>
|
||||
<p id="educationModalStatus" class="text-sm font-medium"></p>
|
||||
</div>
|
||||
<button onclick="closeEducationModal()" 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>
|
||||
<div id="educationModalDescription" class="text-gray-700 dark:text-gray-300 leading-relaxed"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.education-card {
|
||||
animation: fadeInUp 0.6s ease-out forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
.education-card:nth-child(1) { animation-delay: 0.1s; }
|
||||
.education-card:nth-child(2) { animation-delay: 0.2s; }
|
||||
.education-card:nth-child(3) { animation-delay: 0.3s; }
|
||||
.education-card:nth-child(4) { animation-delay: 0.4s; }
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
/* Enhanced styling for education content */
|
||||
.education-content :global(span.font-normal) {
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
font-size: 0.8rem;
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.education-content :global(span.text-sm) {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.8;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
:global(.dark) .education-content :global(span.font-normal) {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 640px) {
|
||||
.education-card {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.education-card .flex {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.education-card .w-12.h-12 {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.education-card .w-6.h-6 {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script is:inline>
|
||||
function showEducationModal(element) {
|
||||
const title = element.dataset.educationTitle;
|
||||
const description = element.dataset.educationDescription;
|
||||
|
||||
// Parse the title to extract clean degree name
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = title;
|
||||
const cleanTitle = tempDiv.textContent || tempDiv.innerText || title;
|
||||
|
||||
// Extract degree type for styling
|
||||
const titleLower = cleanTitle.toLowerCase();
|
||||
let statusText = '';
|
||||
let statusClass = '';
|
||||
|
||||
if (titleLower.includes('bachelor')) {
|
||||
statusText = 'Studies Undertaken';
|
||||
statusClass = 'text-blue-600 dark:text-blue-400';
|
||||
} else if (titleLower.includes('associate')) {
|
||||
statusText = 'Completed';
|
||||
statusClass = 'text-green-600 dark:text-green-400';
|
||||
} else {
|
||||
statusText = 'Completed';
|
||||
statusClass = 'text-purple-600 dark:text-purple-400';
|
||||
}
|
||||
|
||||
// Update modal content
|
||||
document.getElementById('educationModalTitle').innerHTML = title;
|
||||
document.getElementById('educationModalDescription').textContent = description || 'No additional information available.';
|
||||
document.getElementById('educationModalStatus').textContent = statusText;
|
||||
document.getElementById('educationModalStatus').className = `text-sm font-medium ${statusClass}`;
|
||||
|
||||
const modal = document.getElementById('educationModal');
|
||||
modal.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeEducationModal() {
|
||||
const modal = document.getElementById('educationModal');
|
||||
modal.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// Initialize when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Add click listeners to education cards
|
||||
const educationCards = document.querySelectorAll('.education-card');
|
||||
educationCards.forEach(card => {
|
||||
card.addEventListener('click', function() {
|
||||
showEducationModal(this);
|
||||
});
|
||||
});
|
||||
|
||||
// Close modal when clicking outside
|
||||
const educationModal = document.getElementById('educationModal');
|
||||
if (educationModal) {
|
||||
educationModal.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeEducationModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close modal with Escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeEducationModal();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
235
src/components/widgets/ModernSkills.astro
Normal file
235
src/components/widgets/ModernSkills.astro
Normal file
@@ -0,0 +1,235 @@
|
||||
---
|
||||
import Headline from '~/components/ui/Headline.astro';
|
||||
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import type { Features as Props } from '~/types';
|
||||
|
||||
const {
|
||||
title = await Astro.slots.render('title'),
|
||||
subtitle = await Astro.slots.render('subtitle'),
|
||||
tagline = await Astro.slots.render('tagline'),
|
||||
items = [],
|
||||
categories = [],
|
||||
defaultIcon = 'tabler:code',
|
||||
|
||||
id,
|
||||
isDark = false,
|
||||
classes = {},
|
||||
bg = await Astro.slots.render('bg'),
|
||||
} = Astro.props;
|
||||
|
||||
// Group skills by category if categories are provided
|
||||
const skillCategories = categories.length > 0 ? categories : [
|
||||
{
|
||||
title: 'Automation & Integration',
|
||||
icon: 'tabler:settings-automation',
|
||||
color: 'from-blue-500 to-purple-600',
|
||||
skills: items.filter(item => {
|
||||
const title = (item.title || '').toLowerCase();
|
||||
return title.includes('power automate') ||
|
||||
title.includes('copilot studio') ||
|
||||
title.includes('api') ||
|
||||
title.includes('power apps');
|
||||
})
|
||||
},
|
||||
{
|
||||
title: 'Cloud & Infrastructure',
|
||||
icon: 'tabler:cloud',
|
||||
color: 'from-green-500 to-blue-500',
|
||||
skills: items.filter(item => {
|
||||
const title = (item.title || '').toLowerCase();
|
||||
return title.includes('microsoft 365') ||
|
||||
title.includes('sharepoint') ||
|
||||
title.includes('intune') ||
|
||||
title.includes('infrastructure') ||
|
||||
title.includes('infrastructuur') ||
|
||||
(title.includes('beheer') && !title.includes('nexthink'));
|
||||
})
|
||||
},
|
||||
{
|
||||
title: 'Administration & Support',
|
||||
icon: 'tabler:settings',
|
||||
color: 'from-orange-500 to-red-500',
|
||||
skills: items.filter(item => {
|
||||
const title = (item.title || '').toLowerCase();
|
||||
return title.includes('powershell') ||
|
||||
title.includes('nexthink') ||
|
||||
title.includes('itsm') ||
|
||||
title.includes('topdesk') ||
|
||||
title.includes('support') ||
|
||||
title.includes('line');
|
||||
})
|
||||
}
|
||||
];
|
||||
|
||||
// Fallback for uncategorized skills
|
||||
const uncategorizedSkills = items.filter(item =>
|
||||
!skillCategories.some(category =>
|
||||
category.skills.some(skill => skill.title === item.title)
|
||||
)
|
||||
);
|
||||
|
||||
if (uncategorizedSkills.length > 0) {
|
||||
skillCategories.push({
|
||||
title: 'Other Skills',
|
||||
icon: 'tabler:code',
|
||||
color: 'from-gray-500 to-gray-600',
|
||||
skills: uncategorizedSkills
|
||||
});
|
||||
}
|
||||
---
|
||||
|
||||
<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="mt-8 space-y-8">
|
||||
{
|
||||
skillCategories.map(({ title: categoryTitle, icon, color, skills }) => (
|
||||
<div class="skill-category">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class={`w-8 h-8 rounded-lg bg-gradient-to-r ${color} flex items-center justify-center mr-3`}>
|
||||
<Icon name={icon} class="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">{categoryTitle}</h3>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
{skills.map(({ title: skillTitle, description }) => (
|
||||
<div
|
||||
class={`skill-pill bg-gradient-to-r ${color} text-white px-6 py-3 rounded-full font-semibold text-sm cursor-pointer transition-all duration-300 hover:transform hover:scale-105 hover:shadow-lg relative overflow-hidden group`}
|
||||
data-skill-title={skillTitle}
|
||||
data-skill-description={description}
|
||||
>
|
||||
<span class="relative z-10">{skillTitle}</span>
|
||||
<div class="absolute inset-0 bg-white opacity-0 group-hover:opacity-20 transition-opacity duration-300"></div>
|
||||
<div class="absolute top-0 left-[-100%] w-full h-full bg-gradient-to-r from-transparent via-white/30 to-transparent group-hover:left-[100%] transition-all duration-500"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</WidgetWrapper>
|
||||
|
||||
<!-- Skill Details Modal -->
|
||||
<div id="skillModal" 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-lg w-full mx-4 transform scale-90 opacity-0 transition-all duration-300">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<h3 id="skillModalTitle" class="text-2xl font-bold text-gray-900 dark:text-white"></h3>
|
||||
<button onclick="closeSkillModal()" 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="skillModalDescription" class="text-gray-700 dark:text-gray-300 leading-relaxed"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.skill-pill {
|
||||
position: relative;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.skill-pill:hover {
|
||||
transform: translateY(-3px) scale(1.05);
|
||||
}
|
||||
|
||||
.skill-pill:active {
|
||||
transform: translateY(-1px) scale(1.02);
|
||||
}
|
||||
|
||||
.modal {
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.modal.active {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.modal.active .modal-content {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.skill-category {
|
||||
animation: fadeInUp 0.6s ease-out forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
.skill-category:nth-child(1) { animation-delay: 0.1s; }
|
||||
.skill-category:nth-child(2) { animation-delay: 0.2s; }
|
||||
.skill-category:nth-child(3) { animation-delay: 0.3s; }
|
||||
.skill-category:nth-child(4) { animation-delay: 0.4s; }
|
||||
|
||||
@keyframes fadeInUp {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.skill-pill {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script is:inline>
|
||||
function showSkillModal(element) {
|
||||
const title = element.dataset.skillTitle;
|
||||
const description = element.dataset.skillDescription;
|
||||
|
||||
document.getElementById('skillModalTitle').textContent = title;
|
||||
document.getElementById('skillModalDescription').textContent = description;
|
||||
|
||||
const modal = document.getElementById('skillModal');
|
||||
modal.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeSkillModal() {
|
||||
const modal = document.getElementById('skillModal');
|
||||
modal.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// Initialize when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Add click listeners to skill pills
|
||||
const skillPills = document.querySelectorAll('.skill-pill');
|
||||
skillPills.forEach(pill => {
|
||||
pill.addEventListener('click', function() {
|
||||
showSkillModal(this);
|
||||
});
|
||||
});
|
||||
|
||||
// Close modal when clicking outside
|
||||
const skillModal = document.getElementById('skillModal');
|
||||
if (skillModal) {
|
||||
skillModal.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeSkillModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close modal with Escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeSkillModal();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
224
src/components/widgets/ModernWorkExperience.astro
Normal file
224
src/components/widgets/ModernWorkExperience.astro
Normal file
@@ -0,0 +1,224 @@
|
||||
---
|
||||
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||
import Headline from '~/components/ui/Headline.astro';
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import type { Widget } from '~/types';
|
||||
|
||||
export interface Props extends Widget {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
tagline?: string;
|
||||
compact?: boolean;
|
||||
items?: Array<{
|
||||
title: string;
|
||||
description?: string;
|
||||
company?: string;
|
||||
date?: string;
|
||||
location?: string;
|
||||
icon?: string;
|
||||
classes?: Record<string, string>;
|
||||
}>;
|
||||
}
|
||||
|
||||
const {
|
||||
title = 'Work Experience',
|
||||
subtitle = 'My professional journey',
|
||||
tagline = '',
|
||||
compact = false,
|
||||
items = [],
|
||||
id,
|
||||
isDark = false,
|
||||
classes = {},
|
||||
bg = '',
|
||||
} = Astro.props as Props;
|
||||
|
||||
// Function to get gradient based on company or role
|
||||
const getWorkGradient = (company: string = '', title: string = '') => {
|
||||
const companyLower = company.toLowerCase();
|
||||
const titleLower = title.toLowerCase();
|
||||
|
||||
if (companyLower.includes('cofra')) return 'from-blue-600 to-indigo-700';
|
||||
if (companyLower.includes('hyva')) return 'from-green-600 to-teal-700';
|
||||
if (companyLower.includes('bergsma')) return 'from-purple-600 to-pink-700';
|
||||
if (companyLower.includes('allseas')) return 'from-orange-600 to-red-700';
|
||||
if (companyLower.includes('oz export')) return 'from-cyan-600 to-blue-700';
|
||||
|
||||
// Fallback based on role level
|
||||
if (titleLower.includes('manager')) return 'from-indigo-600 to-purple-700';
|
||||
if (titleLower.includes('professional')) return 'from-blue-600 to-cyan-700';
|
||||
if (titleLower.includes('engineer')) return 'from-green-600 to-blue-700';
|
||||
if (titleLower.includes('consultant')) return 'from-purple-600 to-indigo-700';
|
||||
if (titleLower.includes('administrator')) return 'from-gray-600 to-slate-700';
|
||||
|
||||
return 'from-gray-600 to-gray-700';
|
||||
};
|
||||
|
||||
// Function to get appropriate icon
|
||||
const getWorkIcon = (title: string = '') => {
|
||||
const titleLower = title.toLowerCase();
|
||||
|
||||
if (titleLower.includes('manager')) return 'tabler:user-star';
|
||||
if (titleLower.includes('professional')) return 'tabler:certificate';
|
||||
if (titleLower.includes('engineer')) return 'tabler:code';
|
||||
if (titleLower.includes('consultant')) return 'tabler:user-check';
|
||||
if (titleLower.includes('administrator')) return 'tabler:settings';
|
||||
|
||||
return 'tabler:briefcase';
|
||||
};
|
||||
|
||||
// Function to extract years from date range
|
||||
const getYearRange = (dateStr: string = '') => {
|
||||
if (!dateStr) return '';
|
||||
|
||||
// Handle formats like "02-2025 - Present" or "04-2018 - 09-2018"
|
||||
const match = dateStr.match(/(\d{2})-(\d{4})\s*-\s*(.+)/);
|
||||
if (match) {
|
||||
const startYear = match[2];
|
||||
const endPart = match[3].trim();
|
||||
|
||||
if (endPart.toLowerCase().includes('present') || endPart.toLowerCase().includes('heden')) {
|
||||
return `${startYear} - Present`;
|
||||
} else {
|
||||
const endMatch = endPart.match(/(\d{2})-(\d{4})/);
|
||||
if (endMatch) {
|
||||
return `${startYear} - ${endMatch[2]}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dateStr;
|
||||
};
|
||||
---
|
||||
|
||||
<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 lg:grid-cols-2 gap-6 mt-8">
|
||||
{items.map((item, index) => {
|
||||
const gradient = getWorkGradient(item.company, item.title);
|
||||
const icon = getWorkIcon(item.title);
|
||||
const yearRange = getYearRange(item.date);
|
||||
|
||||
return (
|
||||
<div
|
||||
class="work-card group bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm rounded-2xl p-6 transition-all duration-300 cursor-default border border-gray-100 dark:border-slate-800 hover:transform hover:scale-[1.02] hover:shadow-xl relative overflow-hidden"
|
||||
>
|
||||
<!-- Top gradient bar -->
|
||||
<div class={`absolute top-0 left-0 right-0 h-1 bg-gradient-to-r ${gradient}`}></div>
|
||||
|
||||
<!-- Header with Icon, Title & Company -->
|
||||
<div class="flex items-start gap-4 mb-4">
|
||||
<div class={`flex-shrink-0 w-12 h-12 rounded-xl bg-gradient-to-r ${gradient} flex items-center justify-center group-hover:scale-110 transition-transform duration-300 shadow-lg`}>
|
||||
<Icon name={icon} class="w-6 h-6 text-white" />
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-semibold text-lg text-gray-900 dark:text-white mb-1 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 leading-snug">
|
||||
{item.title}
|
||||
</h3>
|
||||
{item.company && (
|
||||
<p class={`font-medium text-transparent bg-clip-text bg-gradient-to-r ${gradient} text-sm mb-1`}>
|
||||
{item.company}
|
||||
</p>
|
||||
)}
|
||||
<div class="flex flex-wrap gap-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
{yearRange && (
|
||||
<span class="flex items-center gap-1">
|
||||
<Icon name="tabler:calendar" class="w-3 h-3" />
|
||||
{yearRange}
|
||||
</span>
|
||||
)}
|
||||
{item.location && (
|
||||
<span class="flex items-center gap-1">
|
||||
<Icon name="tabler:map-pin" class="w-3 h-3" />
|
||||
{item.location}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
{item.description && (
|
||||
<div class="work-description">
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||
{item.description}
|
||||
</p>
|
||||
</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>
|
||||
</WidgetWrapper>
|
||||
|
||||
<style>
|
||||
.work-card {
|
||||
animation: fadeInUp 0.6s ease-out forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
.work-card:nth-child(1) { animation-delay: 0.1s; }
|
||||
.work-card:nth-child(2) { animation-delay: 0.15s; }
|
||||
.work-card:nth-child(3) { animation-delay: 0.2s; }
|
||||
.work-card:nth-child(4) { animation-delay: 0.25s; }
|
||||
.work-card:nth-child(5) { animation-delay: 0.3s; }
|
||||
.work-card:nth-child(6) { animation-delay: 0.35s; }
|
||||
|
||||
@keyframes fadeInUp {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced description styling */
|
||||
.work-description {
|
||||
max-height: 120px;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease-out;
|
||||
}
|
||||
|
||||
.work-card:hover .work-description {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 1024px) {
|
||||
.work-card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.work-card {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.work-card .flex {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.work-card .w-12.h-12 {
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
}
|
||||
|
||||
.work-card .w-6.h-6 {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user