- Updated the Logo component to improve accessibility and visual design, replacing SVG elements with structured HTML and CSS for better responsiveness and styling. - Introduced a new translations interface for applied skills, adding detailed descriptions and links for various skills across multiple languages. - Enhanced the about me page to include a new section for applied skills, integrating the updated translations and improving the overall user experience.
314 lines
12 KiB
Plaintext
314 lines
12 KiB
Plaintext
---
|
|
import Headline from '~/components/ui/Headline.astro';
|
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
|
import Button from '~/components/ui/Button.astro';
|
|
import { Icon } from 'astro-icon/components';
|
|
import type { Testimonials as Props } from '~/types';
|
|
|
|
const {
|
|
title = '',
|
|
subtitle = '',
|
|
tagline = '',
|
|
testimonials = [],
|
|
callToAction,
|
|
|
|
id,
|
|
isDark = false,
|
|
classes = {},
|
|
bg = await Astro.slots.render('bg'),
|
|
} = Astro.props;
|
|
|
|
// Function to get the correct skill icon based on title
|
|
const getSkillIcon = (name: string): string => {
|
|
const nameLower = name.toLowerCase();
|
|
|
|
if (nameLower.includes('entra') || nameLower.includes('identity')) return 'tabler:shield-lock';
|
|
if (nameLower.includes('power automate') || nameLower.includes('automated processes')) return 'tabler:robot';
|
|
if (nameLower.includes('copilot') || nameLower.includes('agents')) return 'tabler:brain';
|
|
|
|
return 'tabler:certificate';
|
|
};
|
|
|
|
// Function to get the correct gradient colors based on skill
|
|
const getSkillGradient = (name: string): string => {
|
|
const nameLower = name.toLowerCase();
|
|
|
|
if (nameLower.includes('entra') || nameLower.includes('identity')) return 'from-blue-600 to-indigo-700';
|
|
if (nameLower.includes('power automate') || nameLower.includes('automated processes')) return 'from-purple-600 to-pink-600';
|
|
if (nameLower.includes('copilot') || nameLower.includes('agents')) return 'from-green-600 to-teal-600';
|
|
|
|
return 'from-gray-500 to-gray-600';
|
|
};
|
|
---
|
|
|
|
<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 gap-4 mt-8">
|
|
{
|
|
testimonials &&
|
|
testimonials.map(({ linkUrl, name, issueDate, description }) => {
|
|
const icon = getSkillIcon(name || '');
|
|
const gradient = getSkillGradient(name || '');
|
|
|
|
return (
|
|
<div
|
|
class="applied-skill-card 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 group"
|
|
data-applied-skill-title={name}
|
|
data-applied-skill-date={issueDate}
|
|
data-applied-skill-description={description}
|
|
data-applied-skill-url={linkUrl}
|
|
itemscope
|
|
itemtype="https://schema.org/EducationalOccupationalCredential"
|
|
>
|
|
<!-- Top gradient bar -->
|
|
<div class={`absolute top-0 left-0 right-0 h-1 bg-gradient-to-r ${gradient}`}></div>
|
|
|
|
<!-- Microsoft Applied Skills Badge -->
|
|
<div class="flex items-center justify-center mb-4">
|
|
<div class={`px-3 py-1 rounded-full text-xs font-medium text-white bg-gradient-to-r ${gradient} shadow-sm`}>
|
|
Microsoft Applied Skills
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="text-center">
|
|
<h3
|
|
class="font-semibold text-base text-gray-900 dark:text-white mb-3 line-clamp-3 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-relaxed"
|
|
itemprop="name"
|
|
>
|
|
{name}
|
|
</h3>
|
|
<p
|
|
class={`text-sm font-medium text-transparent bg-clip-text bg-gradient-to-r ${gradient}`}
|
|
itemprop="validIn"
|
|
>
|
|
{issueDate}
|
|
</p>
|
|
<meta itemprop="credentialCategory" content="Microsoft Applied Skills" />
|
|
<meta itemprop="recognizedBy" content="Microsoft" />
|
|
{linkUrl && <meta itemprop="url" content={linkUrl} />}
|
|
</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-black/10 to-transparent dark:via-white/10 -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>
|
|
|
|
<!-- Applied Skills Details Modal -->
|
|
<div id="appliedSkillModal" class="applied-skill-modal fixed inset-0 bg-black/50 backdrop-blur-sm z-50 hidden items-center justify-center p-4">
|
|
<div class="applied-skill-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="appliedSkillModalTitle" class="text-2xl font-bold text-gray-900 dark:text-white mb-1"></h3>
|
|
<p id="appliedSkillModalDate" class="text-sm text-blue-600 dark:text-blue-400 font-medium"></p>
|
|
</div>
|
|
<button onclick="closeAppliedSkillModal()" 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="appliedSkillModalDescription" class="text-gray-700 dark:text-gray-300 leading-relaxed mb-6"></p>
|
|
<div class="flex justify-end">
|
|
<a id="appliedSkillModalLink" 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 Applied Skill
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.applied-skill-card {
|
|
animation: fadeInUp 0.6s ease-out forwards;
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
}
|
|
|
|
.applied-skill-card:nth-child(1) { animation-delay: 0.1s; }
|
|
.applied-skill-card:nth-child(2) { animation-delay: 0.15s; }
|
|
.applied-skill-card:nth-child(3) { animation-delay: 0.2s; }
|
|
|
|
@keyframes fadeInUp {
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.applied-skill-modal {
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.applied-skill-modal.active {
|
|
display: flex !important;
|
|
}
|
|
|
|
.applied-skill-modal.active .applied-skill-modal-content {
|
|
transform: scale(1);
|
|
opacity: 1;
|
|
}
|
|
|
|
.line-clamp-3 {
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 3;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
</style>
|
|
|
|
<script is:inline>
|
|
// Applied Skills widget touch handling - completely isolated to avoid conflicts
|
|
(function() {
|
|
let appliedSkillTouchStart = { x: 0, y: 0, time: 0 };
|
|
let appliedSkillIsScrolling = false;
|
|
let appliedSkillOriginalScrollPosition = 0;
|
|
let appliedSkillClickedElement = null;
|
|
|
|
function appliedSkillHandleTouchStart(e) {
|
|
const touch = e.touches[0];
|
|
appliedSkillTouchStart = {
|
|
x: touch.clientX,
|
|
y: touch.clientY,
|
|
time: Date.now()
|
|
};
|
|
appliedSkillIsScrolling = false;
|
|
}
|
|
|
|
function appliedSkillHandleTouchMove(e) {
|
|
const touch = e.touches[0];
|
|
const deltaX = Math.abs(touch.clientX - appliedSkillTouchStart.x);
|
|
const deltaY = Math.abs(touch.clientY - appliedSkillTouchStart.y);
|
|
|
|
if (deltaX > 10 || deltaY > 10) {
|
|
appliedSkillIsScrolling = true;
|
|
}
|
|
}
|
|
|
|
function appliedSkillHandleTouchEnd(e, element) {
|
|
const touch = e.changedTouches[0];
|
|
const touchEndTime = Date.now();
|
|
|
|
if (appliedSkillIsScrolling) return;
|
|
|
|
const touchDuration = touchEndTime - appliedSkillTouchStart.time;
|
|
if (touchDuration > 300) return;
|
|
|
|
const deltaX = Math.abs(touch.clientX - appliedSkillTouchStart.x);
|
|
const deltaY = Math.abs(touch.clientY - appliedSkillTouchStart.y);
|
|
if (deltaX > 15 || deltaY > 15) return;
|
|
|
|
e.preventDefault();
|
|
showAppliedSkillModal(element);
|
|
}
|
|
|
|
window.showAppliedSkillModal = function(element) {
|
|
// Store the clicked element and current scroll position
|
|
appliedSkillClickedElement = element;
|
|
appliedSkillOriginalScrollPosition = window.pageYOffset || document.documentElement.scrollTop;
|
|
|
|
const title = element.dataset.appliedSkillTitle;
|
|
const date = element.dataset.appliedSkillDate;
|
|
const description = element.dataset.appliedSkillDescription;
|
|
const url = element.dataset.appliedSkillUrl;
|
|
|
|
document.getElementById('appliedSkillModalTitle').textContent = title;
|
|
document.getElementById('appliedSkillModalDate').textContent = date;
|
|
document.getElementById('appliedSkillModalDescription').textContent = description;
|
|
document.getElementById('appliedSkillModalLink').href = url;
|
|
|
|
const modal = document.getElementById('appliedSkillModal');
|
|
modal.classList.add('active');
|
|
|
|
setTimeout(() => {
|
|
const modalContent = modal.querySelector('.applied-skill-modal-content');
|
|
if (modalContent) {
|
|
modalContent.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
}, 100);
|
|
};
|
|
|
|
window.closeAppliedSkillModal = function() {
|
|
const modal = document.getElementById('appliedSkillModal');
|
|
modal.classList.remove('active');
|
|
|
|
// Restore scroll position to the original card
|
|
if (appliedSkillClickedElement) {
|
|
setTimeout(() => {
|
|
// First, scroll to the original position
|
|
window.scrollTo({
|
|
top: appliedSkillOriginalScrollPosition,
|
|
behavior: 'smooth'
|
|
});
|
|
|
|
// Then, ensure the clicked card is visible with a slight offset
|
|
setTimeout(() => {
|
|
const elementRect = appliedSkillClickedElement.getBoundingClientRect();
|
|
const isElementVisible = elementRect.top >= 0 && elementRect.bottom <= window.innerHeight;
|
|
|
|
if (!isElementVisible) {
|
|
appliedSkillClickedElement.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'center'
|
|
});
|
|
}
|
|
}, 300);
|
|
}, 100);
|
|
}
|
|
|
|
// Clear the reference
|
|
appliedSkillClickedElement = null;
|
|
};
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Select only applied skill cards with the specific class and data attributes
|
|
const appliedSkillCards = document.querySelectorAll('.applied-skill-card[data-applied-skill-title]');
|
|
|
|
appliedSkillCards.forEach(card => {
|
|
card.addEventListener('click', function(e) {
|
|
if (!('ontouchstart' in window)) {
|
|
e.preventDefault();
|
|
showAppliedSkillModal(this);
|
|
}
|
|
});
|
|
|
|
card.addEventListener('touchstart', appliedSkillHandleTouchStart, { passive: true });
|
|
card.addEventListener('touchmove', appliedSkillHandleTouchMove, { passive: true });
|
|
card.addEventListener('touchend', function(e) {
|
|
appliedSkillHandleTouchEnd(e, this);
|
|
}, { passive: false });
|
|
});
|
|
|
|
const appliedSkillModal = document.getElementById('appliedSkillModal');
|
|
if (appliedSkillModal) {
|
|
appliedSkillModal.addEventListener('click', function(e) {
|
|
if (e.target === this) {
|
|
closeAppliedSkillModal();
|
|
}
|
|
});
|
|
}
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') {
|
|
closeAppliedSkillModal();
|
|
}
|
|
});
|
|
});
|
|
})(); |