Main page overhaul
This commit is contained in:
@@ -25,7 +25,7 @@ export default defineConfig({
|
|||||||
output: 'static',
|
output: 'static',
|
||||||
|
|
||||||
i18n: {
|
i18n: {
|
||||||
locales: ["en", "de", "nl"],
|
locales: ["en", "de", "nl", "fr"],
|
||||||
defaultLocale: "en",
|
defaultLocale: "en",
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ export default defineConfig({
|
|||||||
removeAttributeQuotes: false,
|
removeAttributeQuotes: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Image: false,
|
Image: true,
|
||||||
JavaScript: true,
|
JavaScript: true,
|
||||||
SVG: false,
|
SVG: false,
|
||||||
Logger: 1,
|
Logger: 1,
|
||||||
|
@@ -20,6 +20,12 @@ import '@fontsource-variable/inter';
|
|||||||
---
|
---
|
||||||
|
|
||||||
<style is:inline>
|
<style is:inline>
|
||||||
|
/* Optimize font loading with font-display:swap */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter Variable';
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--aw-font-sans: 'Inter Variable';
|
--aw-font-sans: 'Inter Variable';
|
||||||
--aw-font-serif: 'Inter Variable';
|
--aw-font-serif: 'Inter Variable';
|
||||||
@@ -31,7 +37,7 @@ import '@fontsource-variable/inter';
|
|||||||
|
|
||||||
--aw-color-text-heading: rgb(0 0 0);
|
--aw-color-text-heading: rgb(0 0 0);
|
||||||
--aw-color-text-default: rgb(16 16 16);
|
--aw-color-text-default: rgb(16 16 16);
|
||||||
--aw-color-text-muted: rgb(16 16 16 / 66%);
|
--aw-color-text-muted: rgb(16 16 16 / 40%);
|
||||||
--aw-color-bg-page: rgb(255 255 255);
|
--aw-color-bg-page: rgb(255 255 255);
|
||||||
|
|
||||||
--aw-color-bg-page-dark: rgb(3 6 32);
|
--aw-color-bg-page-dark: rgb(3 6 32);
|
||||||
@@ -52,7 +58,7 @@ import '@fontsource-variable/inter';
|
|||||||
|
|
||||||
--aw-color-text-heading: rgb(247, 248, 248);
|
--aw-color-text-heading: rgb(247, 248, 248);
|
||||||
--aw-color-text-default: rgb(229 236 246);
|
--aw-color-text-default: rgb(229 236 246);
|
||||||
--aw-color-text-muted: rgb(229 236 246 / 66%);
|
--aw-color-text-muted: rgb(229 236 246 / 85%);
|
||||||
--aw-color-bg-page: rgb(3 6 32);
|
--aw-color-bg-page: rgb(3 6 32);
|
||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
|
@@ -23,12 +23,13 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
|
|||||||
|
|
||||||
<div class="relative inline-block text-left">
|
<div class="relative inline-block text-left">
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="inline-flex justify-center w-full rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-800 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-800 focus:ring-indigo-500 dark:focus:ring-indigo-400 transition-colors duration-200"
|
class="inline-flex justify-center w-full rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-800 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-800 focus:ring-indigo-500 dark:focus:ring-indigo-400 focus-visible:ring-4 transition-colors duration-200"
|
||||||
id="menu-button"
|
id="menu-button"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
|
aria-label={`Select language. Current language: ${currentLanguage.name}`}
|
||||||
>
|
>
|
||||||
<Icon name={`circle-flags:${currentLanguage.flag}`} class="inline-block w-5 h-5 mr-2" />
|
<Icon name={`circle-flags:${currentLanguage.flag}`} class="inline-block w-5 h-5 mr-2" />
|
||||||
<span id="selected-language">{currentLanguage.name}</span>
|
<span id="selected-language">{currentLanguage.name}</span>
|
||||||
@@ -55,9 +56,10 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-lang-code={lang.code}
|
data-lang-code={lang.code}
|
||||||
class="text-gray-700 dark:text-gray-300 block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white transition-colors duration-200"
|
class="text-gray-700 dark:text-gray-300 block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white focus:bg-gray-100 dark:focus:bg-gray-700 focus:text-gray-900 dark:focus:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-400 transition-colors duration-200"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
|
aria-label={`Switch to ${lang.name} language`}
|
||||||
>
|
>
|
||||||
<Icon name={`circle-flags:${lang.flag}`} class="inline-block w-5 h-5 mr-2" />
|
<Icon name={`circle-flags:${lang.flag}`} class="inline-block w-5 h-5 mr-2" />
|
||||||
{lang.name}
|
{lang.name}
|
||||||
@@ -188,6 +190,11 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
|
|||||||
closeMenu();
|
closeMenu();
|
||||||
} else {
|
} else {
|
||||||
openMenu();
|
openMenu();
|
||||||
|
// Focus the first menu item for better keyboard navigation
|
||||||
|
const firstMenuItem = menu.querySelector('button[role="menuitem"]');
|
||||||
|
if (firstMenuItem) {
|
||||||
|
(firstMenuItem as HTMLElement).focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -236,6 +243,22 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
|
|||||||
closeMenu();
|
closeMenu();
|
||||||
button.focus();
|
button.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enhanced keyboard navigation with arrow keys
|
||||||
|
if (isOpen && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
|
||||||
|
e.preventDefault();
|
||||||
|
const menuItems = Array.from(menu.querySelectorAll('button[role="menuitem"]'));
|
||||||
|
const currentIndex = menuItems.findIndex(item => item === document.activeElement);
|
||||||
|
|
||||||
|
let newIndex;
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
newIndex = currentIndex < menuItems.length - 1 ? currentIndex + 1 : 0;
|
||||||
|
} else {
|
||||||
|
newIndex = currentIndex > 0 ? currentIndex - 1 : menuItems.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
(menuItems[newIndex] as HTMLElement).focus();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,4 +5,9 @@ import { getAsset } from '~/utils/permalinks';
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<!-- Resource hints to improve performance -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link rel="preload" href="/fonts/inter-variable.woff2" as="font" type="font/woff2" crossorigin />
|
||||||
|
|
||||||
<link rel="sitemap" href={getAsset('/sitemap-index.xml')} />
|
<link rel="sitemap" href={getAsset('/sitemap-index.xml')} />
|
||||||
|
5
src/components/common/StructuredData.astro
Normal file
5
src/components/common/StructuredData.astro
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
const { data } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<script type="application/ld+json" set:html={JSON.stringify(data)} />
|
69
src/components/ui/CompactTimeline.astro
Normal file
69
src/components/ui/CompactTimeline.astro
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import type { Item } from '~/types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
items?: Array<Item>;
|
||||||
|
defaultIcon?: string;
|
||||||
|
classes?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { items = [], classes = {}, defaultIcon } = Astro.props as Props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
container: containerClass = '',
|
||||||
|
panel: panelClass = '',
|
||||||
|
title: titleClass = '',
|
||||||
|
description: descriptionClass = '',
|
||||||
|
icon: defaultIconClass = 'text-primary dark:text-slate-200 border-primary dark:border-blue-700',
|
||||||
|
} = classes;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
items && items.length && (
|
||||||
|
<div class={twMerge("grid grid-cols-1 md:grid-cols-2 gap-4", containerClass)}>
|
||||||
|
{items.map(({ title, description, icon, classes: itemClasses = {} }) => (
|
||||||
|
<div
|
||||||
|
class={twMerge(
|
||||||
|
'flex intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:shadow-md transition-all duration-300 ease-in-out hover:bg-gray-50 dark:hover:bg-gray-800',
|
||||||
|
panelClass,
|
||||||
|
itemClasses?.panel
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div class="flex-shrink-0 mr-3 rtl:mr-0 rtl:ml-3">
|
||||||
|
{(icon || defaultIcon) && (
|
||||||
|
<Icon
|
||||||
|
name={icon || defaultIcon}
|
||||||
|
class={twMerge('w-8 h-8 p-1.5 rounded-full border-2', defaultIconClass, itemClasses?.icon)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{title && <p class={twMerge('text-lg font-bold', titleClass, itemClasses?.title)} set:html={title} />}
|
||||||
|
{description && (
|
||||||
|
<div class="text-muted mt-2 overflow-hidden">
|
||||||
|
<div
|
||||||
|
class={twMerge('text-sm max-h-[4.5rem] hover:max-h-[300px] transition-all duration-500 ease-description', descriptionClass, itemClasses?.description)}
|
||||||
|
set:html={description}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Custom easing function for description expansion */
|
||||||
|
.ease-description {
|
||||||
|
transition-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fade effect for the gradient overlay */
|
||||||
|
.hover\:opacity-0:hover {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
169
src/components/ui/ImageModal.astro
Normal file
169
src/components/ui/ImageModal.astro
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
---
|
||||||
|
// ImageModal.astro - A reusable modal component for displaying enlarged images
|
||||||
|
---
|
||||||
|
|
||||||
|
<div id="image-modal" class="fixed inset-0 z-50 flex items-center justify-center opacity-0 pointer-events-none transition-opacity duration-300 ease-in-out">
|
||||||
|
<!-- Backdrop overlay -->
|
||||||
|
<div id="modal-backdrop" class="absolute inset-0 bg-black bg-opacity-75 backdrop-blur-sm transition-opacity duration-300"></div>
|
||||||
|
|
||||||
|
<!-- Modal container with animation -->
|
||||||
|
<div id="modal-container" class="relative max-w-4xl mx-auto p-4 transform scale-95 transition-all duration-300 ease-in-out flex flex-col items-center">
|
||||||
|
<!-- Close button -->
|
||||||
|
<button
|
||||||
|
id="modal-close"
|
||||||
|
class="absolute top-2 right-2 z-10 bg-white dark:bg-gray-800 rounded-full p-2 shadow-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors duration-200"
|
||||||
|
aria-label="Close modal"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-600 dark:text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Image container -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden" style="min-height: 300px; max-height: 75vh;">
|
||||||
|
<div class="flex items-center justify-center h-full w-full">
|
||||||
|
<img
|
||||||
|
id="modal-image"
|
||||||
|
src=""
|
||||||
|
alt="Enlarged certificate"
|
||||||
|
class="w-auto object-contain"
|
||||||
|
style="max-height: var(--cert-max-height, 75%); vertical-align: middle;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Caption -->
|
||||||
|
<div id="modal-caption" class="mt-2 text-center text-white text-lg font-medium"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Declare the global function type
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
openImageModal: (imgSrc: string, imgAlt: string) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize modal functionality when the DOM is loaded
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const modal = document.getElementById('image-modal');
|
||||||
|
const modalBackdrop = document.getElementById('modal-backdrop');
|
||||||
|
const modalContainer = document.getElementById('modal-container');
|
||||||
|
const modalImage = document.getElementById('modal-image') as HTMLImageElement;
|
||||||
|
const modalCaption = document.getElementById('modal-caption');
|
||||||
|
const closeButton = document.getElementById('modal-close');
|
||||||
|
|
||||||
|
// Function to open the modal with a specific image
|
||||||
|
function openModal(imgSrc, imgAlt) {
|
||||||
|
if (!modal || !modalImage || !modalCaption) return;
|
||||||
|
|
||||||
|
// Create a temporary image to get the natural dimensions
|
||||||
|
const tempImg = new Image();
|
||||||
|
tempImg.onload = function() {
|
||||||
|
// Calculate the certification section height (75% of viewport height as defined in the container)
|
||||||
|
const certSectionHeight = window.innerHeight * 0.75;
|
||||||
|
const maxHeight = certSectionHeight * 0.75; // 75% of the certification section height
|
||||||
|
|
||||||
|
// If the natural image height is smaller than the max height, use the natural height
|
||||||
|
if (tempImg.height < maxHeight) {
|
||||||
|
modalImage.style.setProperty('--cert-max-height', `${tempImg.height}px`);
|
||||||
|
} else {
|
||||||
|
modalImage.style.setProperty('--cert-max-height', `${maxHeight}px`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the image source and alt text
|
||||||
|
modalImage.src = imgSrc;
|
||||||
|
modalCaption.textContent = imgAlt;
|
||||||
|
|
||||||
|
// Make the modal visible
|
||||||
|
modal.classList.remove('opacity-0', 'pointer-events-none');
|
||||||
|
modal.classList.add('opacity-100', 'pointer-events-auto');
|
||||||
|
|
||||||
|
// Animate the container
|
||||||
|
if (modalContainer) {
|
||||||
|
modalContainer.classList.remove('scale-95');
|
||||||
|
modalContainer.classList.add('scale-100');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set focus to the close button for accessibility
|
||||||
|
if (closeButton) {
|
||||||
|
setTimeout(() => closeButton.focus(), 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start loading the image
|
||||||
|
tempImg.src = imgSrc;
|
||||||
|
|
||||||
|
// Prevent scrolling on the body
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to close the modal
|
||||||
|
function closeModal() {
|
||||||
|
if (!modal || !modalContainer) return;
|
||||||
|
|
||||||
|
// Hide the modal with animation
|
||||||
|
modal.classList.remove('opacity-100', 'pointer-events-auto');
|
||||||
|
modal.classList.add('opacity-0', 'pointer-events-none');
|
||||||
|
|
||||||
|
// Animate the container
|
||||||
|
modalContainer.classList.remove('scale-100');
|
||||||
|
modalContainer.classList.add('scale-95');
|
||||||
|
|
||||||
|
// Re-enable scrolling
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
|
||||||
|
// Clear the image source after animation completes
|
||||||
|
setTimeout(() => {
|
||||||
|
if (modalImage) modalImage.src = '';
|
||||||
|
if (modalCaption) modalCaption.textContent = '';
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the openImageModal function globally so it can be called from anywhere
|
||||||
|
window.openImageModal = openModal;
|
||||||
|
|
||||||
|
// Close modal when clicking the close button
|
||||||
|
if (closeButton) {
|
||||||
|
closeButton.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
closeModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal when clicking outside the image container
|
||||||
|
if (modal) {
|
||||||
|
modal.addEventListener('click', (e) => {
|
||||||
|
// Check if the click was on the modal backdrop or the modal itself (not on its children)
|
||||||
|
if (e.target === modal || e.target === modalBackdrop) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal when pressing Escape key
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle window resize to recalculate image dimensions
|
||||||
|
let resizeTimeout;
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
clearTimeout(resizeTimeout);
|
||||||
|
resizeTimeout = setTimeout(() => {
|
||||||
|
if (modalImage && modalImage.src && modal && modal.classList.contains('opacity-100')) {
|
||||||
|
// Get current image source and recalculate dimensions
|
||||||
|
const currentSrc = modalImage.src;
|
||||||
|
const currentAlt = modalCaption?.textContent || '';
|
||||||
|
|
||||||
|
// Close and reopen modal to trigger recalculation
|
||||||
|
closeModal();
|
||||||
|
setTimeout(() => openModal(currentSrc, currentAlt), 350);
|
||||||
|
}
|
||||||
|
}, 200); // Debounce resize events
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
164
src/components/ui/ModernTimeline.astro
Normal file
164
src/components/ui/ModernTimeline.astro
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import type { Item } from '~/types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
items?: Array<Item & { year?: string }>;
|
||||||
|
defaultIcon?: string;
|
||||||
|
classes?: Record<string, string>;
|
||||||
|
compact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { items = [], classes = {}, defaultIcon, compact = false } = Astro.props as Props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
container: containerClass = '',
|
||||||
|
panel: panelClass = '',
|
||||||
|
title: titleClass = '',
|
||||||
|
description: descriptionClass = '',
|
||||||
|
icon: defaultIconClass = 'text-primary dark:text-slate-200 border-primary dark:border-blue-700',
|
||||||
|
timeline: timelineClass = 'bg-primary/30 dark:bg-blue-700/30',
|
||||||
|
timelineDot: timelineDotClass = 'bg-primary dark:bg-blue-700',
|
||||||
|
year: yearClass = 'text-primary dark:text-blue-300',
|
||||||
|
} = classes;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
items && items.length && (
|
||||||
|
<div class={twMerge("relative mx-auto max-w-5xl", containerClass)}>
|
||||||
|
{/* Main timeline line */}
|
||||||
|
<div class="absolute left-4 md:left-1/2 top-0 h-full w-1 transform -translate-x-1/2 z-0 transition-all duration-300 ease-in-out" class:list={[timelineClass]}></div>
|
||||||
|
|
||||||
|
<div class="relative">
|
||||||
|
{items.map((item, index) => {
|
||||||
|
const { title, description, icon, classes: itemClasses = {} } = item;
|
||||||
|
const isEven = index % 2 === 0;
|
||||||
|
|
||||||
|
// Use the year property if available, otherwise try to extract from date
|
||||||
|
let year = item.year;
|
||||||
|
|
||||||
|
// If year is not provided, try to extract from date in the description
|
||||||
|
if (!year && description) {
|
||||||
|
// Look for a date pattern like MM-YYYY
|
||||||
|
const dateMatch = description.match(/\d{2}-(\d{4})/);
|
||||||
|
if (dateMatch) {
|
||||||
|
year = dateMatch[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={`relative ${compact ? 'mb-6' : 'mb-12'}`}>
|
||||||
|
{/* Year marker (if available) */}
|
||||||
|
{year && (
|
||||||
|
<div class={twMerge("absolute left-4 md:left-1/2 transform -translate-x-1/2 -top-4 font-bold text-xs z-10", yearClass)}>
|
||||||
|
{year}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Timeline dot */}
|
||||||
|
<div class={`absolute left-4 md:left-1/2 transform -translate-x-1/2 top-5 ${compact ? 'w-3 h-3' : 'w-4 h-4'} rounded-full z-10 shadow-md transition-all duration-300 ease-in-out`} class:list={[timelineDotClass]}></div>
|
||||||
|
|
||||||
|
{/* Content card */}
|
||||||
|
<div
|
||||||
|
class={twMerge(
|
||||||
|
'relative ml-10 md:ml-0 md:w-[45%]',
|
||||||
|
isEven ? 'md:mr-auto md:pr-6' : 'md:ml-auto md:pl-6',
|
||||||
|
'transition-all duration-300 ease-in-out'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class={twMerge(
|
||||||
|
`flex flex-col border border-gray-200 dark:border-gray-700 rounded-lg ${compact ? 'p-3' : 'p-4'} shadow-sm hover:shadow-md transition-all duration-300 ease-in-out bg-white dark:bg-gray-900 hover:translate-y-[-3px] group card-container`,
|
||||||
|
panelClass,
|
||||||
|
itemClasses?.panel
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
{(icon || defaultIcon) && (
|
||||||
|
<Icon
|
||||||
|
name={icon || defaultIcon}
|
||||||
|
class={twMerge(`${compact ? 'w-6 h-6 p-1' : 'w-7 h-7 p-1.5'} rounded-full border-2 mr-2 flex-shrink-0`, defaultIconClass, itemClasses?.icon)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{title && <p class={twMerge(`${compact ? 'text-base' : 'text-lg'} font-bold`, titleClass, itemClasses?.title)} set:html={title} />}
|
||||||
|
</div>
|
||||||
|
{description && (
|
||||||
|
<div class="max-h-0 group-hover:max-h-[500px] overflow-hidden transition-all duration-500 ease-in-out" data-details>
|
||||||
|
<div class="flex items-center justify-center mb-1 opacity-70 group-hover:opacity-0 transition-opacity duration-200 h-4">
|
||||||
|
<div class="w-6 h-1 bg-gray-300 dark:bg-gray-700 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={twMerge(`text-muted ${compact ? 'text-xs' : 'text-sm'} opacity-0 group-hover:opacity-100 transition-all duration-500 ease-in-out`, descriptionClass, itemClasses?.description)}
|
||||||
|
set:html={description}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Connector line to timeline (visible only on desktop) */}
|
||||||
|
<div class={twMerge(
|
||||||
|
'absolute top-5 hidden md:block h-0.5 w-6 z-0',
|
||||||
|
isEven ? 'right-0 bg-gradient-to-r' : 'left-0 bg-gradient-to-l',
|
||||||
|
'from-transparent to-primary/70 dark:to-blue-700/70'
|
||||||
|
)}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Add some animation classes */
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card container styles */
|
||||||
|
.card-container {
|
||||||
|
min-height: 0;
|
||||||
|
height: auto;
|
||||||
|
transition: transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94), box-shadow 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
will-change: transform, box-shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover effect for details */
|
||||||
|
[data-details] {
|
||||||
|
transform-origin: top;
|
||||||
|
transition: max-height 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||||||
|
opacity 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
will-change: max-height, opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add a subtle indicator that more content is available */
|
||||||
|
.group:not(:hover) [data-details] {
|
||||||
|
max-height: 0 !important;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group:hover [data-details] {
|
||||||
|
max-height: 500px; /* Large enough to fit content but still allows animation */
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.timeline-item {
|
||||||
|
width: calc(100% - 2.5rem);
|
||||||
|
margin-left: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
128
src/components/ui/StaggeredTimeline.astro
Normal file
128
src/components/ui/StaggeredTimeline.astro
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import type { Item } from '~/types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
items?: Array<Item>;
|
||||||
|
defaultIcon?: string;
|
||||||
|
classes?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { items = [], classes = {}, defaultIcon } = Astro.props as Props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
container: containerClass = '',
|
||||||
|
panel: panelClass = '',
|
||||||
|
title: titleClass = '',
|
||||||
|
description: descriptionClass = '',
|
||||||
|
icon: defaultIconClass = 'text-primary dark:text-slate-200 border-primary dark:border-blue-700',
|
||||||
|
arrow: arrowClass = 'text-primary dark:text-slate-200',
|
||||||
|
} = classes;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
items && items.length && (
|
||||||
|
<div class={twMerge("relative mx-auto max-w-5xl pt-8", containerClass)}>
|
||||||
|
{/* Mobile timeline line */}
|
||||||
|
<div class="absolute left-4 top-8 h-full w-1 bg-primary/30 dark:bg-blue-700/30 md:hidden"></div>
|
||||||
|
|
||||||
|
<div class="relative min-h-screen">
|
||||||
|
{items.map((item, index) => {
|
||||||
|
const { title, description, icon, classes: itemClasses = {} } = item;
|
||||||
|
const isEven = index % 2 === 0;
|
||||||
|
const isFirst = index === 0;
|
||||||
|
|
||||||
|
// Calculate vertical offset based on position with consistent spacing
|
||||||
|
const offsetValue = index * 8;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={`relative ${isEven ? 'ml-0 mr-auto' : 'ml-auto mr-0'} w-full md:w-[45%] mb-12`}
|
||||||
|
style={!isFirst ? `margin-top: ${offsetValue}rem;` : ''}
|
||||||
|
id={`timeline-item-${index}`}
|
||||||
|
>
|
||||||
|
{/* Arrow connecting to previous item (except for first item) */}
|
||||||
|
{!isFirst && (
|
||||||
|
<div class="absolute hidden md:block">
|
||||||
|
{isEven ? (
|
||||||
|
<div class={twMerge("absolute -left-16 -top-16 w-32 h-32", arrowClass)}>
|
||||||
|
<svg class="w-full h-full" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M20 20 C 40 20, 60 80, 80 80" stroke="currentColor" stroke-width="2.5" fill="none" />
|
||||||
|
<path d="M80 80 L 70 72" stroke="currentColor" stroke-width="2.5" />
|
||||||
|
<path d="M80 80 L 72 90" stroke="currentColor" stroke-width="2.5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div class={twMerge("absolute -right-16 -top-16 w-32 h-32", arrowClass)}>
|
||||||
|
<svg class="w-full h-full" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M80 20 C 60 20, 40 80, 20 80" stroke="currentColor" stroke-width="2.5" fill="none" />
|
||||||
|
<path d="M20 80 L 30 72" stroke="currentColor" stroke-width="2.5" />
|
||||||
|
<path d="M20 80 L 28 90" stroke="currentColor" stroke-width="2.5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Timeline item */}
|
||||||
|
<div
|
||||||
|
class={twMerge(
|
||||||
|
'flex intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade border border-gray-200 dark:border-gray-700 rounded-lg p-5 hover:shadow-md transition-all duration-300 ease-in-out hover:bg-gray-50 dark:hover:bg-gray-800 ml-8 md:ml-0 shadow-sm relative',
|
||||||
|
panelClass,
|
||||||
|
itemClasses?.panel
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div class="flex-shrink-0 mr-4 rtl:mr-0 rtl:ml-4">
|
||||||
|
{(icon || defaultIcon) && (
|
||||||
|
<Icon
|
||||||
|
name={icon || defaultIcon}
|
||||||
|
class={twMerge('w-10 h-10 p-2 rounded-full border-2 shadow-sm', defaultIconClass, itemClasses?.icon)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{title && <p class={twMerge('text-lg font-bold', titleClass, itemClasses?.title)} set:html={title} />}
|
||||||
|
{description && (
|
||||||
|
<div class="text-muted mt-2 overflow-hidden">
|
||||||
|
<div
|
||||||
|
class={twMerge('text-sm max-h-[2.5rem] md:max-h-none hover:max-h-[300px] transition-all duration-500 ease-staggered', descriptionClass, itemClasses?.description)}
|
||||||
|
set:html={description}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Responsive styles for small screens */}
|
||||||
|
<style>
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#timeline-item-0,
|
||||||
|
#timeline-item-1,
|
||||||
|
#timeline-item-2,
|
||||||
|
#timeline-item-3,
|
||||||
|
#timeline-item-4 {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
margin-left: 2rem !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
width: calc(100% - 2rem) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom easing function for staggered timeline */
|
||||||
|
.ease-staggered {
|
||||||
|
transition-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fade effect for the gradient overlay */
|
||||||
|
.hover\:opacity-0:hover {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
105
src/components/widgets/CompactCertifications.astro
Normal file
105
src/components/widgets/CompactCertifications.astro
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
---
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
import ImageModal from '~/components/ui/ImageModal.astro';
|
||||||
|
import type { Testimonials as Props } from '~/types';
|
||||||
|
import DefaultImage from '~/assets/images/default.png';
|
||||||
|
|
||||||
|
// Function to get the correct image path for a testimonial
|
||||||
|
const getImagePath = (image: unknown) => {
|
||||||
|
if (typeof image === 'object' && image !== null && 'src' in image && typeof (image as { src: unknown }).src === 'string') {
|
||||||
|
// If the image has a src property, use it
|
||||||
|
return String((image as { src: string }).src);
|
||||||
|
}
|
||||||
|
// Otherwise, return the default image path
|
||||||
|
return DefaultImage.src;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to get the alt text for an image
|
||||||
|
const getImageAlt = (image: unknown, fallback: string = "Certification badge") => {
|
||||||
|
if (typeof image === 'object' && image !== null && 'alt' in image && typeof (image as { alt: unknown }).alt === 'string') {
|
||||||
|
return String((image as { alt: string }).alt);
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = '',
|
||||||
|
subtitle = '',
|
||||||
|
tagline = '',
|
||||||
|
testimonials = [],
|
||||||
|
callToAction,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<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 md:grid-cols-3 lg:grid-cols-4 gap-4 mt-6">
|
||||||
|
{
|
||||||
|
testimonials &&
|
||||||
|
testimonials.map(({ linkUrl, name, issueDate, description, image }) => (
|
||||||
|
<div class="flex flex-col p-3 rounded-md shadow-md dark:shadow-none dark:border dark:border-slate-600 hover:shadow-lg transition-all duration-300 ease-in-out hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||||
|
<div class="flex items-center mb-3">
|
||||||
|
<div
|
||||||
|
class="h-12 w-12 mr-3 flex-shrink-0 bg-gray-100 dark:bg-gray-800 rounded-md flex items-center justify-center overflow-hidden cursor-pointer"
|
||||||
|
onclick={`window.openImageModal('${getImagePath(image)}', ${JSON.stringify(name || "Certification badge")})`}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={getImagePath(image)}
|
||||||
|
alt={getImageAlt(image, name || "Certification badge")}
|
||||||
|
class="h-10 w-10 object-contain transition-transform duration-300 hover:scale-110"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<a href={linkUrl} target="_blank" rel="noopener noreferrer" class="flex-1">
|
||||||
|
<div>
|
||||||
|
{name && <p class="text-sm font-semibold line-clamp-2">{name}</p>}
|
||||||
|
{issueDate && <p class="text-xs text-muted">{issueDate}</p>}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href={linkUrl} target="_blank" rel="noopener noreferrer" class="block">
|
||||||
|
<div class="text-xs text-muted overflow-hidden">
|
||||||
|
<div class="max-h-[8rem] hover:max-h-[300px] transition-all duration-500 ease-cert">
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Include the image modal component */}
|
||||||
|
<ImageModal />
|
||||||
|
|
||||||
|
{
|
||||||
|
callToAction && (
|
||||||
|
<div class="flex justify-center mx-auto w-fit mt-8 font-medium">
|
||||||
|
<Button {...callToAction} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</WidgetWrapper>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Custom easing function for certification description expansion */
|
||||||
|
.ease-cert {
|
||||||
|
transition-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fade effect for the gradient overlay */
|
||||||
|
.hover\:opacity-0:hover {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
57
src/components/widgets/CompactSkills.astro
Normal file
57
src/components/widgets/CompactSkills.astro
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
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 = [],
|
||||||
|
defaultIcon = 'tabler:point-filled',
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<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-6 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
|
{items.map(({ title, description, icon }) => (
|
||||||
|
<div class="group bg-white dark:bg-slate-800 p-3 rounded-lg shadow-sm hover:shadow-md transition-all duration-300 ease-in-out hover:bg-gray-50 dark:hover:bg-gray-800 border border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<Icon name={icon || defaultIcon} class="w-5 h-5 mr-2 text-primary transition-transform duration-300 group-hover:scale-110" />
|
||||||
|
<h3 class="text-base font-semibold">{title}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted overflow-hidden">
|
||||||
|
<p class="text-xs max-h-[8rem] hover:max-h-[300px] transition-all duration-500 ease-skills">{description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</WidgetWrapper>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Custom easing function for skills expansion */
|
||||||
|
.ease-skills {
|
||||||
|
transition-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fade effect for the gradient overlay */
|
||||||
|
.hover\:opacity-0:hover {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
34
src/components/widgets/CompactSteps.astro
Normal file
34
src/components/widgets/CompactSteps.astro
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import CompactTimeline from '~/components/ui/CompactTimeline.astro';
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
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;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-7xl ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<Headline
|
||||||
|
title={title}
|
||||||
|
subtitle={subtitle}
|
||||||
|
tagline={tagline}
|
||||||
|
classes={{
|
||||||
|
container: 'text-left rtl:text-right max-w-3xl',
|
||||||
|
title: 'text-3xl lg:text-4xl',
|
||||||
|
...((classes?.headline as object) ?? {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CompactTimeline items={items} classes={classes?.items as Record<string, never>} />
|
||||||
|
</div>
|
||||||
|
</WidgetWrapper>
|
@@ -16,14 +16,14 @@ interface Links {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
links: Array<Links>;
|
links?: Array<Links>;
|
||||||
secondaryLinks: Array<Link>;
|
secondaryLinks: Array<Link>;
|
||||||
socialLinks: Array<Link>;
|
socialLinks: Array<Link>;
|
||||||
footNote?: string;
|
footNote?: string;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { socialLinks = [], secondaryLinks = [], links = [], footNote = '', theme = 'light' } = Astro.props;
|
const { socialLinks = [], theme = 'light' } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<footer class:list={[{ dark: theme === 'dark' }, 'relative border-t border-gray-200 dark:border-slate-800 not-prose']}>
|
<footer class:list={[{ dark: theme === 'dark' }, 'relative border-t border-gray-200 dark:border-slate-800 not-prose']}>
|
||||||
@@ -52,8 +52,8 @@ const { socialLinks = [], secondaryLinks = [], links = [], footNote = '', theme
|
|||||||
{
|
{
|
||||||
socialLinks?.length && (
|
socialLinks?.length && (
|
||||||
<ul class="flex space-x-4">
|
<ul class="flex space-x-4">
|
||||||
{socialLinks.map(({ ariaLabel, href, icon }, index) => (
|
{socialLinks.map(({ ariaLabel, href, icon }) => (
|
||||||
<li key={index}>
|
<li>
|
||||||
<a
|
<a
|
||||||
class="text-muted dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg p-2 inline-flex items-center"
|
class="text-muted dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg p-2 inline-flex items-center"
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
|
@@ -75,6 +75,17 @@ const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`;
|
|||||||
<div class="flex items-center md:hidden">
|
<div class="flex items-center md:hidden">
|
||||||
<ToggleMenu />
|
<ToggleMenu />
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Improved mobile navigation accessibility -->
|
||||||
|
<style>
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
nav ul li a, nav ul li button {
|
||||||
|
padding: 0.75rem 1rem; /* Larger touch targets */
|
||||||
|
min-height: 44px; /* Minimum touch target size */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</div>
|
</div>
|
||||||
<nav
|
<nav
|
||||||
class="items-center w-full md:w-auto hidden md:flex md:mx-5 text-default overflow-y-auto overflow-x-hidden md:overflow-y-visible md:overflow-x-auto md:justify-self-center"
|
class="items-center w-full md:w-auto hidden md:flex md:mx-5 text-default overflow-y-auto overflow-x-hidden md:overflow-y-visible md:overflow-x-auto md:justify-self-center"
|
||||||
|
82
src/components/widgets/WorkExperience.astro
Normal file
82
src/components/widgets/WorkExperience.astro
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
import ModernTimeline from '~/components/ui/ModernTimeline.astro';
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Transform the work experience items to the format expected by ModernTimeline
|
||||||
|
const timelineItems = items.map(item => {
|
||||||
|
// Extract year from date if available
|
||||||
|
let year: string | undefined = undefined;
|
||||||
|
if (item.date) {
|
||||||
|
// The date format in translations is like "04-2018 - 09-2018" or "02-2025 - Present"
|
||||||
|
// We want to extract the first year (start year)
|
||||||
|
const dateMatch = item.date.match(/\d{2}-(\d{4})/);
|
||||||
|
if (dateMatch) {
|
||||||
|
year = dateMatch[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `<span class="${compact ? 'text-lg' : 'text-xl'}">${item.title}</span>${item.company ? `<span class="block ${compact ? 'text-xs' : 'text-sm'} font-normal text-gray-600 dark:text-gray-400">${item.company}</span>` : ''}`,
|
||||||
|
description: `<div class="transform-origin-top transition-transform duration-300">${item.description || ''}${item.description && (item.date || item.location) ? '<div class="mt-2"></div>' : ''}${item.date ? `<span class="block text-xs font-semibold text-gray-500 dark:text-gray-400">${item.date}</span>` : ''}${item.location ? `<span class="block text-xs text-gray-500 dark:text-gray-400">${item.location}</span>` : ''}</div>`,
|
||||||
|
icon: item.icon || 'tabler:briefcase',
|
||||||
|
classes: item.classes,
|
||||||
|
year,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} bg={bg} classes={classes}>
|
||||||
|
<div class={`flex flex-col gap-${compact ? '8' : '12'} md:gap-${compact ? '12' : '16'}`}>
|
||||||
|
{title && (
|
||||||
|
<div class="flex flex-col gap-4 text-center">
|
||||||
|
{tagline && (
|
||||||
|
<p class="text-sm font-semibold uppercase tracking-wide text-primary dark:text-blue-200">{tagline}</p>
|
||||||
|
)}
|
||||||
|
{title && <h2 class={`${compact ? 'text-2xl md:text-3xl' : 'text-3xl md:text-4xl'} font-bold font-heading`}>{title}</h2>}
|
||||||
|
{subtitle && <p class={`${compact ? 'text-lg' : 'text-xl'} text-muted dark:text-slate-400`}>{subtitle}</p>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ModernTimeline
|
||||||
|
items={timelineItems}
|
||||||
|
compact={compact}
|
||||||
|
classes={{
|
||||||
|
container: 'max-w-4xl mx-auto px-4',
|
||||||
|
title: 'text-xl font-bold',
|
||||||
|
description: 'text-muted mt-2',
|
||||||
|
icon: 'text-primary dark:text-slate-200 border-primary dark:border-blue-700',
|
||||||
|
timeline: 'bg-primary/30 dark:bg-blue-700/30',
|
||||||
|
timelineDot: 'bg-primary dark:bg-blue-700',
|
||||||
|
year: 'text-primary dark:text-blue-300',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</WidgetWrapper>
|
@@ -2,7 +2,7 @@ site:
|
|||||||
name: 365DevNet
|
name: 365DevNet
|
||||||
site: 'https://www.365devnet.eu'
|
site: 'https://www.365devnet.eu'
|
||||||
base: '/'
|
base: '/'
|
||||||
trailingSlash: false
|
trailingSlash: ignore
|
||||||
|
|
||||||
googleSiteVerificationId: orcPxI47GSa-cRvY11tUe6iGg2IO_RPvnA1q95iEM3M
|
googleSiteVerificationId: orcPxI47GSa-cRvY11tUe6iGg2IO_RPvnA1q95iEM3M
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ metadata:
|
|||||||
openGraph:
|
openGraph:
|
||||||
site_name: 365DevNet
|
site_name: 365DevNet
|
||||||
images:
|
images:
|
||||||
- url: '~/assets/images/default.png'
|
- url: '~/assets/images/richardbergsma.png'
|
||||||
width: 1200
|
width: 1200
|
||||||
height: 628
|
height: 628
|
||||||
type: website
|
type: website
|
||||||
@@ -38,7 +38,7 @@ apps:
|
|||||||
|
|
||||||
post:
|
post:
|
||||||
isEnabled: true
|
isEnabled: true
|
||||||
permalink: '/%slug%' # Variables: %slug%, %year%, %month%, %day%, %hour%, %minute%, %second%, %category%
|
permalink: 'blog/%slug%' # Variables: %slug%, %year%, %month%, %day%, %hour%, %minute%, %second%, %category%
|
||||||
robots:
|
robots:
|
||||||
index: true
|
index: true
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
export interface Translation {
|
export interface Translation {
|
||||||
metadata: {
|
metadata: {
|
||||||
title: string;
|
title: string;
|
||||||
|
aboutUs: string;
|
||||||
};
|
};
|
||||||
navigation: {
|
navigation: {
|
||||||
home: string;
|
home: string;
|
||||||
@@ -8,6 +9,7 @@ export interface Translation {
|
|||||||
resume: string;
|
resume: string;
|
||||||
certifications: string;
|
certifications: string;
|
||||||
skills: string;
|
skills: string;
|
||||||
|
education: string;
|
||||||
blog: string;
|
blog: string;
|
||||||
};
|
};
|
||||||
hero: {
|
hero: {
|
||||||
@@ -74,6 +76,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
en: {
|
en: {
|
||||||
metadata: {
|
metadata: {
|
||||||
title: 'About me',
|
title: 'About me',
|
||||||
|
aboutUs: 'About us',
|
||||||
},
|
},
|
||||||
navigation: {
|
navigation: {
|
||||||
home: 'Home',
|
home: 'Home',
|
||||||
@@ -81,6 +84,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
resume: 'Resume',
|
resume: 'Resume',
|
||||||
certifications: 'Certifications',
|
certifications: 'Certifications',
|
||||||
skills: 'Skills',
|
skills: 'Skills',
|
||||||
|
education: 'Education',
|
||||||
blog: 'Blog',
|
blog: 'Blog',
|
||||||
},
|
},
|
||||||
hero: {
|
hero: {
|
||||||
@@ -195,7 +199,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Certified Entry-Level Python Programmer',
|
name: 'Python Programmer (PCEP)',
|
||||||
issueDate: 'Date Issued: 11-2023',
|
issueDate: 'Date Issued: 11-2023',
|
||||||
description: 'Earning the PCEP™ certification demonstrates proficiency in fundamental Python programming concepts, including data types, control flow, data collections, functions, and exception handling.',
|
description: 'Earning the PCEP™ certification demonstrates proficiency in fundamental Python programming concepts, including data types, control flow, data collections, functions, and exception handling.',
|
||||||
linkUrl: 'https://pythoninstitute.org/pcep',
|
linkUrl: 'https://pythoninstitute.org/pcep',
|
||||||
@@ -206,7 +210,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Modern Desktop Administrator Associate',
|
name: 'Desktop Administrator Associate',
|
||||||
issueDate: 'Date Issued: 06-2023',
|
issueDate: 'Date Issued: 06-2023',
|
||||||
description: 'Earning the Modern Desktop Administrator Associate certification demonstrates proficiency in deploying, configuring, securing, managing, and monitoring devices and client applications within an enterprise environment.',
|
description: 'Earning the Modern Desktop Administrator Associate certification demonstrates proficiency in deploying, configuring, securing, managing, and monitoring devices and client applications within an enterprise environment.',
|
||||||
linkUrl: 'https://learn.microsoft.com/en-us/credentials/certifications/modern-desktop/?practice-assessment-type=certification',
|
linkUrl: 'https://learn.microsoft.com/en-us/credentials/certifications/modern-desktop/?practice-assessment-type=certification',
|
||||||
@@ -276,7 +280,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
description: 'Adept at setting up, managing, and optimizing both SharePoint Online and on-premise deployments, ensuring effective document management, collaboration, and information sharing within organizations.',
|
description: 'Adept at setting up, managing, and optimizing both SharePoint Online and on-premise deployments, ensuring effective document management, collaboration, and information sharing within organizations.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Nexthink Platform Administration',
|
title: 'Nexthink Administration',
|
||||||
description: 'Proficient in administering the Nexthink platform, utilizing its capabilities for IT infrastructure monitoring, executing remote actions, and developing workflows to enhance IT service delivery and user experience.',
|
description: 'Proficient in administering the Nexthink platform, utilizing its capabilities for IT infrastructure monitoring, executing remote actions, and developing workflows to enhance IT service delivery and user experience.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -291,6 +295,18 @@ export const translations: Record<string, Translation> = {
|
|||||||
title: 'ITSM (TOPDesk)',
|
title: 'ITSM (TOPDesk)',
|
||||||
description: 'Experienced in managing ITSM processes using TOPdesk. Proficient in core functionalities such as Incident Management and Asset Management, while leveraging API usage for seamless integrations with other systems.',
|
description: 'Experienced in managing ITSM processes using TOPdesk. Proficient in core functionalities such as Incident Management and Asset Management, while leveraging API usage for seamless integrations with other systems.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'PowerShell',
|
||||||
|
description: 'Proficient in utilizing PowerShell for automation, system administration, and configuration management across Microsoft environments. Experienced in creating robust scripts for task automation, system monitoring, and integration with various Microsoft services.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Intune Device Management',
|
||||||
|
description: 'Skilled in deploying, configuring, and managing Windows 10/11 devices through Microsoft Intune. Experienced in creating and implementing device policies, application deployment, and security configurations for enterprise environments.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '3rd Line IT Support',
|
||||||
|
description: 'Experienced in providing advanced technical support for complex IT issues that require in-depth knowledge and specialized expertise. Proficient in troubleshooting, diagnosing, and resolving critical system problems across various platforms and applications.',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
blog: {
|
blog: {
|
||||||
@@ -301,6 +317,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
nl: {
|
nl: {
|
||||||
metadata: {
|
metadata: {
|
||||||
title: 'Over mij',
|
title: 'Over mij',
|
||||||
|
aboutUs: 'Over ons',
|
||||||
},
|
},
|
||||||
navigation: {
|
navigation: {
|
||||||
home: 'Home',
|
home: 'Home',
|
||||||
@@ -308,6 +325,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
resume: 'CV',
|
resume: 'CV',
|
||||||
certifications: 'Certificeringen',
|
certifications: 'Certificeringen',
|
||||||
skills: 'Vaardigheden',
|
skills: 'Vaardigheden',
|
||||||
|
education: 'Opleiding',
|
||||||
blog: 'Blog',
|
blog: 'Blog',
|
||||||
},
|
},
|
||||||
hero: {
|
hero: {
|
||||||
@@ -389,7 +407,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
subtitle: 'Waar kennis op erkenning stuit',
|
subtitle: 'Waar kennis op erkenning stuit',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Gecertificeerde Nexthink Professional',
|
name: 'Certified Nexthink Professional',
|
||||||
issueDate: 'Datum van afgifte: 01-2025',
|
issueDate: 'Datum van afgifte: 01-2025',
|
||||||
description: 'Het behalen van de Nexthink Certified Application Experience Management-certificering bevestigt de expertise in het optimaliseren van applicatieprestaties, het zorgen voor een naadloze gebruikersacceptatie en het bevorderen van kostenefficiëntie.',
|
description: 'Het behalen van de Nexthink Certified Application Experience Management-certificering bevestigt de expertise in het optimaliseren van applicatieprestaties, het zorgen voor een naadloze gebruikersacceptatie en het bevorderen van kostenefficiëntie.',
|
||||||
linkUrl: 'https://certified.nexthink.com/babd1e3a-c593-4a81-90a2-6a002f43e692#acc.fUOog9dj',
|
linkUrl: 'https://certified.nexthink.com/babd1e3a-c593-4a81-90a2-6a002f43e692#acc.fUOog9dj',
|
||||||
@@ -400,7 +418,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Gecertificeerde Nexthink-beheerder',
|
name: 'Certified Nexthink Administrator',
|
||||||
issueDate: 'Datum van afgifte: 11-2024',
|
issueDate: 'Datum van afgifte: 11-2024',
|
||||||
description: 'Het behalen van de Nexthink Platform Administration-certificering toont aan dat men bekwaam is in het configureren en aanpassen van het Nexthink-platform om aan de behoeften van de organisatie te voldoen.',
|
description: 'Het behalen van de Nexthink Platform Administration-certificering toont aan dat men bekwaam is in het configureren en aanpassen van het Nexthink-platform om aan de behoeften van de organisatie te voldoen.',
|
||||||
linkUrl: 'https://certified.nexthink.com/8bfc61f2-31b8-45d8-82e7-e4a1df2b915d#acc.7eo6pFxb',
|
linkUrl: 'https://certified.nexthink.com/8bfc61f2-31b8-45d8-82e7-e4a1df2b915d#acc.7eo6pFxb',
|
||||||
@@ -411,7 +429,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Gecertificeerde Nexthink Associate',
|
name: 'Certified Nexthink Associate',
|
||||||
issueDate: 'Datum van afgifte: 11-2024',
|
issueDate: 'Datum van afgifte: 11-2024',
|
||||||
description: 'Het behalen van de Nexthink Infinity Fundamentals-certificering bevestigt uw begrip van het Nexthink Infinity-platform en de rol ervan bij het verbeteren van de digitale werknemerservaring.',
|
description: 'Het behalen van de Nexthink Infinity Fundamentals-certificering bevestigt uw begrip van het Nexthink Infinity-platform en de rol ervan bij het verbeteren van de digitale werknemerservaring.',
|
||||||
linkUrl: 'https://certified.nexthink.com/cf5e9e43-9d95-4dc6-bb95-0f7e0bada9b3#acc.YWDnxiaU',
|
linkUrl: 'https://certified.nexthink.com/cf5e9e43-9d95-4dc6-bb95-0f7e0bada9b3#acc.YWDnxiaU',
|
||||||
@@ -422,7 +440,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Gecertificeerde beginnende Python-programmeur',
|
name: 'Python Programmer (PCEP)',
|
||||||
issueDate: 'Datum van afgifte: 11-2023',
|
issueDate: 'Datum van afgifte: 11-2023',
|
||||||
description: 'Het behalen van de PCEP™-certificering toont aan dat men bedreven is in de fundamentele concepten van Python-programmering, waaronder datatypes, controleflow, gegevensverzamelingen, functies en foutafhandeling.',
|
description: 'Het behalen van de PCEP™-certificering toont aan dat men bedreven is in de fundamentele concepten van Python-programmering, waaronder datatypes, controleflow, gegevensverzamelingen, functies en foutafhandeling.',
|
||||||
linkUrl: 'https://pythoninstitute.org/pcep',
|
linkUrl: 'https://pythoninstitute.org/pcep',
|
||||||
@@ -433,7 +451,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Moderne Desktopbeheerder Associate',
|
name: 'Desktop Administrator Associate',
|
||||||
issueDate: 'Datum van afgifte: 06-2023',
|
issueDate: 'Datum van afgifte: 06-2023',
|
||||||
description: 'Het behalen van de Modern Desktop Administrator Associate-certificering toont aan dat men bedreven is in het implementeren, configureren, beveiligen, beheren en monitoren van apparaten en clientapplicaties binnen een bedrijfsomgeving.',
|
description: 'Het behalen van de Modern Desktop Administrator Associate-certificering toont aan dat men bedreven is in het implementeren, configureren, beveiligen, beheren en monitoren van apparaten en clientapplicaties binnen een bedrijfsomgeving.',
|
||||||
linkUrl: 'https://learn.microsoft.com/en-us/credentials/certifications/modern-desktop/?practice-assessment-type=certification',
|
linkUrl: 'https://learn.microsoft.com/en-us/credentials/certifications/modern-desktop/?practice-assessment-type=certification',
|
||||||
@@ -444,7 +462,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Microsoft 365 Basisprincipes',
|
name: 'Microsoft 365 Fundamentals',
|
||||||
issueDate: 'Datum van afgifte: 05-2023',
|
issueDate: 'Datum van afgifte: 05-2023',
|
||||||
description: 'Het behalen van de Microsoft 365 Certified: Fundamentals-certificering toont fundamentele kennis van cloudgebaseerde oplossingen, waaronder productiviteit, samenwerking, beveiliging, compliance en Microsoft 365-diensten.',
|
description: 'Het behalen van de Microsoft 365 Certified: Fundamentals-certificering toont fundamentele kennis van cloudgebaseerde oplossingen, waaronder productiviteit, samenwerking, beveiliging, compliance en Microsoft 365-diensten.',
|
||||||
linkUrl: 'https://learn.microsoft.com/en-us/credentials/certifications/microsoft-365-fundamentals/?practice-assessment-type=certification',
|
linkUrl: 'https://learn.microsoft.com/en-us/credentials/certifications/microsoft-365-fundamentals/?practice-assessment-type=certification',
|
||||||
@@ -455,7 +473,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Teamsbeheerder Associate',
|
name: 'Teams Administrator Associate',
|
||||||
issueDate: 'Datum van afgifte: 06-2021',
|
issueDate: 'Datum van afgifte: 06-2021',
|
||||||
description: 'Het behalen van de Teams Administrator Associate-certificering toont aan dat u in staat bent om Microsoft Teams te plannen, implementeren, configureren en beheren om efficiënte samenwerking en communicatie binnen een Microsoft 365-omgeving te faciliteren.',
|
description: 'Het behalen van de Teams Administrator Associate-certificering toont aan dat u in staat bent om Microsoft Teams te plannen, implementeren, configureren en beheren om efficiënte samenwerking en communicatie binnen een Microsoft 365-omgeving te faciliteren.',
|
||||||
linkUrl: 'https://learn.microsoft.com/en-us/credentials/certifications/m365-teams-administrator-associate/?practice-assessment-type=certification',
|
linkUrl: 'https://learn.microsoft.com/en-us/credentials/certifications/m365-teams-administrator-associate/?practice-assessment-type=certification',
|
||||||
@@ -466,7 +484,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Azure Basisprincipes',
|
name: 'Azure Fundamentals',
|
||||||
issueDate: 'Datum van afgifte: 01-2020',
|
issueDate: 'Datum van afgifte: 01-2020',
|
||||||
description: 'Het behalen van de Microsoft Certified: Azure Fundamentals-certificering toont fundamentele kennis van cloudconcepten, kern-Azure-diensten en Azure-beheer- en governancefuncties en -hulpmiddelen.',
|
description: 'Het behalen van de Microsoft Certified: Azure Fundamentals-certificering toont fundamentele kennis van cloudconcepten, kern-Azure-diensten en Azure-beheer- en governancefuncties en -hulpmiddelen.',
|
||||||
linkUrl: 'https://learn.microsoft.com/en-us/credentials/certifications/azure-fundamentals/?practice-assessment-type=certification',
|
linkUrl: 'https://learn.microsoft.com/en-us/credentials/certifications/azure-fundamentals/?practice-assessment-type=certification',
|
||||||
@@ -503,7 +521,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
description: 'Bedreven in het opzetten, beheren en optimaliseren van zowel SharePoint Online als on-premise implementaties, waarbij effectief documentbeheer, samenwerking en informatie-uitwisseling binnen organisaties wordt gewaarborgd.',
|
description: 'Bedreven in het opzetten, beheren en optimaliseren van zowel SharePoint Online als on-premise implementaties, waarbij effectief documentbeheer, samenwerking en informatie-uitwisseling binnen organisaties wordt gewaarborgd.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Nexthink Platform-beheer',
|
title: 'Nexthink-beheer',
|
||||||
description: 'Bekwaam in het beheren van het Nexthink-platform, gebruikmakend van de mogelijkheden voor IT-infrastructuurmonitoring, het uitvoeren van externe acties en het ontwikkelen van workflows om IT-serviceverlening en gebruikerservaring te verbeteren.',
|
description: 'Bekwaam in het beheren van het Nexthink-platform, gebruikmakend van de mogelijkheden voor IT-infrastructuurmonitoring, het uitvoeren van externe acties en het ontwikkelen van workflows om IT-serviceverlening en gebruikerservaring te verbeteren.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -518,6 +536,18 @@ export const translations: Record<string, Translation> = {
|
|||||||
title: 'ITSM (TOPDesk)',
|
title: 'ITSM (TOPDesk)',
|
||||||
description: 'Ervaren in het beheren van ITSM-processen met TOPdesk. Bekwaam in kernfunctionaliteiten zoals Incident Management en Asset Management, waarbij API-gebruik wordt benut voor naadloze integraties met andere systemen.',
|
description: 'Ervaren in het beheren van ITSM-processen met TOPdesk. Bekwaam in kernfunctionaliteiten zoals Incident Management en Asset Management, waarbij API-gebruik wordt benut voor naadloze integraties met andere systemen.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'PowerShell',
|
||||||
|
description: 'Bekwaam in het gebruik van PowerShell voor automatisering, systeembeheer en configuratiebeheer in Microsoft-omgevingen. Ervaren in het maken van robuuste scripts voor taakautomatisering, systeemmonitoring en integratie met verschillende Microsoft-diensten.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Intune Apparaatbeheer',
|
||||||
|
description: 'Bekwaam in het implementeren, configureren en beheren van Windows 10/11-apparaten via Microsoft Intune. Ervaren in het maken en implementeren van apparaatbeleid, applicatie-implementatie en beveiligingsconfiguraties voor bedrijfsomgevingen.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '3e Lijns IT-ondersteuning',
|
||||||
|
description: 'Ervaren in het bieden van geavanceerde technische ondersteuning voor complexe IT-problemen die diepgaande kennis en gespecialiseerde expertise vereisen. Bekwaam in het oplossen, diagnosticeren en verhelpen van kritieke systeemproblemen op verschillende platforms en applicaties.',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
blog: {
|
blog: {
|
||||||
@@ -528,6 +558,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
de: {
|
de: {
|
||||||
metadata: {
|
metadata: {
|
||||||
title: 'Über mich',
|
title: 'Über mich',
|
||||||
|
aboutUs: 'Über uns',
|
||||||
},
|
},
|
||||||
navigation: {
|
navigation: {
|
||||||
home: 'Start',
|
home: 'Start',
|
||||||
@@ -535,6 +566,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
resume: 'Lebenslauf',
|
resume: 'Lebenslauf',
|
||||||
certifications: 'Zertifizierungen',
|
certifications: 'Zertifizierungen',
|
||||||
skills: 'Fähigkeiten',
|
skills: 'Fähigkeiten',
|
||||||
|
education: 'Ausbildung',
|
||||||
blog: 'Blog',
|
blog: 'Blog',
|
||||||
},
|
},
|
||||||
hero: {
|
hero: {
|
||||||
@@ -616,7 +648,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
subtitle: 'Wo Wissen auf Anerkennung trifft',
|
subtitle: 'Wo Wissen auf Anerkennung trifft',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Zertifizierter Nexthink Professional',
|
name: 'Certified Nexthink Professional',
|
||||||
issueDate: 'Ausstellungsdatum: 01-2025',
|
issueDate: 'Ausstellungsdatum: 01-2025',
|
||||||
description: 'Der Erwerb der Nexthink Certified Application Experience Management-Zertifizierung bestätigt die Expertise in der Optimierung der Anwendungsleistung, der Gewährleistung einer nahtlosen Benutzerakzeptanz und der Förderung der Kosteneffizienz.',
|
description: 'Der Erwerb der Nexthink Certified Application Experience Management-Zertifizierung bestätigt die Expertise in der Optimierung der Anwendungsleistung, der Gewährleistung einer nahtlosen Benutzerakzeptanz und der Förderung der Kosteneffizienz.',
|
||||||
linkUrl: 'https://certified.nexthink.com/babd1e3a-c593-4a81-90a2-6a002f43e692#acc.fUOog9dj',
|
linkUrl: 'https://certified.nexthink.com/babd1e3a-c593-4a81-90a2-6a002f43e692#acc.fUOog9dj',
|
||||||
@@ -627,7 +659,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Zertifizierter Nexthink Administrator',
|
name: 'Certified Nexthink Administrator',
|
||||||
issueDate: 'Ausstellungsdatum: 11-2024',
|
issueDate: 'Ausstellungsdatum: 11-2024',
|
||||||
description: 'Der Erwerb der Nexthink Platform Administration-Zertifizierung zeigt Kompetenz in der Konfiguration und Anpassung der Nexthink-Plattform zur Erfüllung organisatorischer Anforderungen.',
|
description: 'Der Erwerb der Nexthink Platform Administration-Zertifizierung zeigt Kompetenz in der Konfiguration und Anpassung der Nexthink-Plattform zur Erfüllung organisatorischer Anforderungen.',
|
||||||
linkUrl: 'https://certified.nexthink.com/8bfc61f2-31b8-45d8-82e7-e4a1df2b915d#acc.7eo6pFxb',
|
linkUrl: 'https://certified.nexthink.com/8bfc61f2-31b8-45d8-82e7-e4a1df2b915d#acc.7eo6pFxb',
|
||||||
@@ -638,7 +670,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Zertifizierter Nexthink Associate',
|
name: 'Certified Nexthink Associate',
|
||||||
issueDate: 'Ausstellungsdatum: 11-2024',
|
issueDate: 'Ausstellungsdatum: 11-2024',
|
||||||
description: 'Der Erwerb der Nexthink Infinity Fundamentals-Zertifizierung bestätigt Ihr Verständnis der Nexthink Infinity-Plattform und ihrer Rolle bei der Verbesserung der digitalen Mitarbeitererfahrung.',
|
description: 'Der Erwerb der Nexthink Infinity Fundamentals-Zertifizierung bestätigt Ihr Verständnis der Nexthink Infinity-Plattform und ihrer Rolle bei der Verbesserung der digitalen Mitarbeitererfahrung.',
|
||||||
linkUrl: 'https://certified.nexthink.com/cf5e9e43-9d95-4dc6-bb95-0f7e0bada9b3#acc.YWDnxiaU',
|
linkUrl: 'https://certified.nexthink.com/cf5e9e43-9d95-4dc6-bb95-0f7e0bada9b3#acc.YWDnxiaU',
|
||||||
@@ -649,7 +681,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Zertifizierter Python-Programmierer (Einstiegsniveau)',
|
name: 'Python Programmer (PCEP)',
|
||||||
issueDate: 'Ausstellungsdatum: 11-2023',
|
issueDate: 'Ausstellungsdatum: 11-2023',
|
||||||
description: 'Der Erwerb der PCEP™-Zertifizierung zeigt Kompetenz in grundlegenden Python-Programmierkonzepten, einschließlich Datentypen, Kontrollfluss, Datensammlungen, Funktionen und Fehlerbehandlung.',
|
description: 'Der Erwerb der PCEP™-Zertifizierung zeigt Kompetenz in grundlegenden Python-Programmierkonzepten, einschließlich Datentypen, Kontrollfluss, Datensammlungen, Funktionen und Fehlerbehandlung.',
|
||||||
linkUrl: 'https://pythoninstitute.org/pcep',
|
linkUrl: 'https://pythoninstitute.org/pcep',
|
||||||
@@ -660,7 +692,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Modern Desktop Administrator Associate',
|
name: 'Desktop Administrator Associate',
|
||||||
issueDate: 'Ausstellungsdatum: 06-2023',
|
issueDate: 'Ausstellungsdatum: 06-2023',
|
||||||
description: 'Der Erwerb der Modern Desktop Administrator Associate-Zertifizierung zeigt Kompetenz in der Bereitstellung, Konfiguration, Sicherung, Verwaltung und Überwachung von Geräten und Client-Anwendungen in einer Unternehmensumgebung.',
|
description: 'Der Erwerb der Modern Desktop Administrator Associate-Zertifizierung zeigt Kompetenz in der Bereitstellung, Konfiguration, Sicherung, Verwaltung und Überwachung von Geräten und Client-Anwendungen in einer Unternehmensumgebung.',
|
||||||
linkUrl: 'https://learn.microsoft.com/en-us/credentials/certifications/modern-desktop/?practice-assessment-type=certification',
|
linkUrl: 'https://learn.microsoft.com/en-us/credentials/certifications/modern-desktop/?practice-assessment-type=certification',
|
||||||
@@ -671,7 +703,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Microsoft 365 Grundlagen',
|
name: 'Microsoft 365 Fundamentals',
|
||||||
issueDate: 'Ausstellungsdatum: 05-2023',
|
issueDate: 'Ausstellungsdatum: 05-2023',
|
||||||
description: 'Der Erwerb der Microsoft 365 Certified: Fundamentals-Zertifizierung zeigt grundlegendes Wissen über Cloud-basierte Lösungen, einschließlich Produktivität, Zusammenarbeit, Sicherheit, Compliance und Microsoft 365-Dienste.',
|
description: 'Der Erwerb der Microsoft 365 Certified: Fundamentals-Zertifizierung zeigt grundlegendes Wissen über Cloud-basierte Lösungen, einschließlich Produktivität, Zusammenarbeit, Sicherheit, Compliance und Microsoft 365-Dienste.',
|
||||||
linkUrl: 'https://learn.microsoft.com/en-us/credentials/certifications/microsoft-365-fundamentals/?practice-assessment-type=certification',
|
linkUrl: 'https://learn.microsoft.com/en-us/credentials/certifications/microsoft-365-fundamentals/?practice-assessment-type=certification',
|
||||||
@@ -693,7 +725,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Azure Grundlagen',
|
name: 'Azure Fundamentals',
|
||||||
issueDate: 'Ausstellungsdatum: 01-2020',
|
issueDate: 'Ausstellungsdatum: 01-2020',
|
||||||
description: 'Der Erwerb der Microsoft Certified: Azure Fundamentals-Zertifizierung zeigt grundlegendes Wissen über Cloud-Konzepte, zentrale Azure-Dienste und Azure-Verwaltungs- und Governance-Funktionen und -Tools.',
|
description: 'Der Erwerb der Microsoft Certified: Azure Fundamentals-Zertifizierung zeigt grundlegendes Wissen über Cloud-Konzepte, zentrale Azure-Dienste und Azure-Verwaltungs- und Governance-Funktionen und -Tools.',
|
||||||
linkUrl: 'https://learn.microsoft.com/en-us/credentials/certifications/azure-fundamentals/?practice-assessment-type=certification',
|
linkUrl: 'https://learn.microsoft.com/en-us/credentials/certifications/azure-fundamentals/?practice-assessment-type=certification',
|
||||||
@@ -730,7 +762,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
description: 'Versiert in der Einrichtung, Verwaltung und Optimierung sowohl von SharePoint Online als auch On-Premise-Implementierungen, zur Gewährleistung effektiven Dokumentenmanagements, Zusammenarbeit und Informationsaustausch innerhalb von Organisationen.',
|
description: 'Versiert in der Einrichtung, Verwaltung und Optimierung sowohl von SharePoint Online als auch On-Premise-Implementierungen, zur Gewährleistung effektiven Dokumentenmanagements, Zusammenarbeit und Informationsaustausch innerhalb von Organisationen.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Nexthink Platform Administration',
|
title: 'Nexthink Administration',
|
||||||
description: 'Kompetent in der Verwaltung der Nexthink-Plattform, Nutzung ihrer Fähigkeiten für IT-Infrastrukturüberwachung, Ausführung von Remote-Aktionen und Entwicklung von Workflows zur Verbesserung von IT-Service-Bereitstellung und Benutzererfahrung.',
|
description: 'Kompetent in der Verwaltung der Nexthink-Plattform, Nutzung ihrer Fähigkeiten für IT-Infrastrukturüberwachung, Ausführung von Remote-Aktionen und Entwicklung von Workflows zur Verbesserung von IT-Service-Bereitstellung und Benutzererfahrung.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -745,6 +777,18 @@ export const translations: Record<string, Translation> = {
|
|||||||
title: 'ITSM (TOPDesk)',
|
title: 'ITSM (TOPDesk)',
|
||||||
description: 'Erfahren in der Verwaltung von ITSM-Prozessen mit TOPdesk. Kompetent in Kernfunktionalitäten wie Incident Management und Asset Management, bei gleichzeitiger Nutzung von API-Verwendung für nahtlose Integrationen mit anderen Systemen.',
|
description: 'Erfahren in der Verwaltung von ITSM-Prozessen mit TOPdesk. Kompetent in Kernfunktionalitäten wie Incident Management und Asset Management, bei gleichzeitiger Nutzung von API-Verwendung für nahtlose Integrationen mit anderen Systemen.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'PowerShell',
|
||||||
|
description: 'Kompetent in der Nutzung von PowerShell für Automatisierung, Systemadministration und Konfigurationsmanagement in Microsoft-Umgebungen. Erfahren in der Erstellung robuster Skripte für Aufgabenautomatisierung, Systemüberwachung und Integration mit verschiedenen Microsoft-Diensten.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Intune Geräteverwaltung',
|
||||||
|
description: 'Erfahren in der Bereitstellung, Konfiguration und Verwaltung von Windows 10/11-Geräten über Microsoft Intune. Kompetent in der Erstellung und Implementierung von Geräterichtlinien, Anwendungsbereitstellung und Sicherheitskonfigurationen für Unternehmensumgebungen.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '3rd Line IT-Support',
|
||||||
|
description: 'Erfahren in der Bereitstellung fortgeschrittener technischer Unterstützung für komplexe IT-Probleme, die tiefgreifendes Wissen und spezialisierte Expertise erfordern. Kompetent in der Fehlersuche, Diagnose und Lösung kritischer Systemprobleme über verschiedene Plattformen und Anwendungen hinweg.',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
blog: {
|
blog: {
|
||||||
@@ -755,6 +799,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
fr: {
|
fr: {
|
||||||
metadata: {
|
metadata: {
|
||||||
title: 'À propos de moi',
|
title: 'À propos de moi',
|
||||||
|
aboutUs: 'À propos de nous',
|
||||||
},
|
},
|
||||||
navigation: {
|
navigation: {
|
||||||
home: 'Accueil',
|
home: 'Accueil',
|
||||||
@@ -762,6 +807,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
resume: 'CV',
|
resume: 'CV',
|
||||||
certifications: 'Certifications',
|
certifications: 'Certifications',
|
||||||
skills: 'Compétences',
|
skills: 'Compétences',
|
||||||
|
education: 'Formation',
|
||||||
blog: 'Blog',
|
blog: 'Blog',
|
||||||
},
|
},
|
||||||
hero: {
|
hero: {
|
||||||
@@ -843,7 +889,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
subtitle: 'Où la connaissance rencontre la reconnaissance',
|
subtitle: 'Où la connaissance rencontre la reconnaissance',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Professionnel Nexthink Certifié',
|
name: 'Certified Nexthink Professional',
|
||||||
issueDate: 'Date de délivrance : 01-2025',
|
issueDate: 'Date de délivrance : 01-2025',
|
||||||
description: 'L\'obtention de la certification Nexthink Certified Application Experience Management valide l\'expertise dans l\'optimisation des performances des applications, assurant une adoption transparente par les utilisateurs et favorisant l\'efficacité des coûts.',
|
description: 'L\'obtention de la certification Nexthink Certified Application Experience Management valide l\'expertise dans l\'optimisation des performances des applications, assurant une adoption transparente par les utilisateurs et favorisant l\'efficacité des coûts.',
|
||||||
linkUrl: 'https://certified.nexthink.com/babd1e3a-c593-4a81-90a2-6a002f43e692#acc.fUOog9dj',
|
linkUrl: 'https://certified.nexthink.com/babd1e3a-c593-4a81-90a2-6a002f43e692#acc.fUOog9dj',
|
||||||
@@ -854,7 +900,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Administrateur Nexthink Certifié',
|
name: 'Certified Nexthink Administrator',
|
||||||
issueDate: 'Date de délivrance : 11-2024',
|
issueDate: 'Date de délivrance : 11-2024',
|
||||||
description: 'L\'obtention de la certification Nexthink Platform Administration démontre la compétence dans la configuration et la personnalisation de la plateforme Nexthink pour répondre aux besoins organisationnels.',
|
description: 'L\'obtention de la certification Nexthink Platform Administration démontre la compétence dans la configuration et la personnalisation de la plateforme Nexthink pour répondre aux besoins organisationnels.',
|
||||||
linkUrl: 'https://certified.nexthink.com/8bfc61f2-31b8-45d8-82e7-e4a1df2b915d#acc.7eo6pFxb',
|
linkUrl: 'https://certified.nexthink.com/8bfc61f2-31b8-45d8-82e7-e4a1df2b915d#acc.7eo6pFxb',
|
||||||
@@ -865,7 +911,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Associate Nexthink Certifié',
|
name: 'Certified Nexthink Associate',
|
||||||
issueDate: 'Date de délivrance : 11-2024',
|
issueDate: 'Date de délivrance : 11-2024',
|
||||||
description: 'L\'obtention de la certification Nexthink Infinity Fundamentals valide votre compréhension de la plateforme Nexthink Infinity et de son rôle dans l\'amélioration de l\'expérience numérique des employés.',
|
description: 'L\'obtention de la certification Nexthink Infinity Fundamentals valide votre compréhension de la plateforme Nexthink Infinity et de son rôle dans l\'amélioration de l\'expérience numérique des employés.',
|
||||||
linkUrl: 'https://certified.nexthink.com/cf5e9e43-9d95-4dc6-bb95-0f7e0bada9b3#acc.YWDnxiaU',
|
linkUrl: 'https://certified.nexthink.com/cf5e9e43-9d95-4dc6-bb95-0f7e0bada9b3#acc.YWDnxiaU',
|
||||||
@@ -876,7 +922,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Programmeur Python Certifié Niveau Débutant',
|
name: 'Python Programmer (PCEP)',
|
||||||
issueDate: 'Date de délivrance : 11-2023',
|
issueDate: 'Date de délivrance : 11-2023',
|
||||||
description: 'L\'obtention de la certification PCEP™ démontre la maîtrise des concepts fondamentaux de la programmation Python, y compris les types de données, le contrôle de flux, les collections de données, les fonctions et la gestion des erreurs.',
|
description: 'L\'obtention de la certification PCEP™ démontre la maîtrise des concepts fondamentaux de la programmation Python, y compris les types de données, le contrôle de flux, les collections de données, les fonctions et la gestion des erreurs.',
|
||||||
linkUrl: 'https://pythoninstitute.org/pcep',
|
linkUrl: 'https://pythoninstitute.org/pcep',
|
||||||
@@ -887,7 +933,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Modern Desktop Administrator Associate',
|
name: 'Desktop Administrator Associate',
|
||||||
issueDate: 'Date de délivrance : 06-2023',
|
issueDate: 'Date de délivrance : 06-2023',
|
||||||
description: 'L\'obtention de la certification Modern Desktop Administrator Associate démontre la compétence dans le déploiement, la configuration, la sécurisation, la gestion et la surveillance des appareils et des applications client dans un environnement d\'entreprise.',
|
description: 'L\'obtention de la certification Modern Desktop Administrator Associate démontre la compétence dans le déploiement, la configuration, la sécurisation, la gestion et la surveillance des appareils et des applications client dans un environnement d\'entreprise.',
|
||||||
linkUrl: 'https://learn.microsoft.com/en-us/credentials/certifications/modern-desktop/?practice-assessment-type=certification',
|
linkUrl: 'https://learn.microsoft.com/en-us/credentials/certifications/modern-desktop/?practice-assessment-type=certification',
|
||||||
@@ -957,7 +1003,7 @@ export const translations: Record<string, Translation> = {
|
|||||||
description: 'Compétent dans la configuration, la gestion et l\'optimisation des déploiements SharePoint Online et on-premise, assurant une gestion efficace des documents, la collaboration et le partage d\'informations au sein des organisations.',
|
description: 'Compétent dans la configuration, la gestion et l\'optimisation des déploiements SharePoint Online et on-premise, assurant une gestion efficace des documents, la collaboration et le partage d\'informations au sein des organisations.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Administration Plateforme Nexthink',
|
title: 'Administration Nexthink',
|
||||||
description: 'Compétent dans l\'administration de la plateforme Nexthink, utilisant ses capacités pour la surveillance de l\'infrastructure IT, l\'exécution d\'actions à distance et le développement de workflows pour améliorer la prestation de services IT et l\'expérience utilisateur.',
|
description: 'Compétent dans l\'administration de la plateforme Nexthink, utilisant ses capacités pour la surveillance de l\'infrastructure IT, l\'exécution d\'actions à distance et le développement de workflows pour améliorer la prestation de services IT et l\'expérience utilisateur.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -972,6 +1018,18 @@ export const translations: Record<string, Translation> = {
|
|||||||
title: 'ITSM (TOPDesk)',
|
title: 'ITSM (TOPDesk)',
|
||||||
description: 'Expérimenté dans la gestion des processus ITSM avec TOPdesk. Compétent dans les fonctionnalités principales telles que la gestion des incidents et la gestion des actifs, tout en exploitant l\'utilisation des APIs pour des intégrations transparentes avec d\'autres systèmes.',
|
description: 'Expérimenté dans la gestion des processus ITSM avec TOPdesk. Compétent dans les fonctionnalités principales telles que la gestion des incidents et la gestion des actifs, tout en exploitant l\'utilisation des APIs pour des intégrations transparentes avec d\'autres systèmes.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'PowerShell',
|
||||||
|
description: 'Compétent dans l\'utilisation de PowerShell pour l\'automatisation, l\'administration système et la gestion de configuration dans les environnements Microsoft. Expérimenté dans la création de scripts robustes pour l\'automatisation des tâches, la surveillance des systèmes et l\'intégration avec divers services Microsoft.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Gestion des Appareils Intune',
|
||||||
|
description: 'Compétent dans le déploiement, la configuration et la gestion des appareils Windows 10/11 via Microsoft Intune. Expérimenté dans la création et l\'implémentation de politiques d\'appareils, le déploiement d\'applications et les configurations de sécurité pour les environnements d\'entreprise.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Support IT de 3ème Niveau',
|
||||||
|
description: 'Expérimenté dans la fourniture d\'un support technique avancé pour des problèmes IT complexes nécessitant une connaissance approfondie et une expertise spécialisée. Compétent dans le dépannage, le diagnostic et la résolution de problèmes système critiques sur diverses plateformes et applications.',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
blog: {
|
blog: {
|
||||||
|
@@ -11,6 +11,7 @@ import Metadata from '~/components/common/Metadata.astro';
|
|||||||
import SiteVerification from '~/components/common/SiteVerification.astro';
|
import SiteVerification from '~/components/common/SiteVerification.astro';
|
||||||
import Analytics from '~/components/common/Analytics.astro';
|
import Analytics from '~/components/common/Analytics.astro';
|
||||||
import BasicScripts from '~/components/common/BasicScripts.astro';
|
import BasicScripts from '~/components/common/BasicScripts.astro';
|
||||||
|
import StructuredData from '~/components/common/StructuredData.astro';
|
||||||
|
|
||||||
// Comment the line below to disable View Transitions
|
// Comment the line below to disable View Transitions
|
||||||
import { ClientRouter } from 'astro:transitions';
|
import { ClientRouter } from 'astro:transitions';
|
||||||
@@ -35,12 +36,26 @@ const { language, textDirection } = I18N;
|
|||||||
<Metadata {...metadata} />
|
<Metadata {...metadata} />
|
||||||
<SiteVerification />
|
<SiteVerification />
|
||||||
<Analytics />
|
<Analytics />
|
||||||
|
|
||||||
|
<!-- Structured Data for SEO -->
|
||||||
|
<StructuredData data={{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebSite",
|
||||||
|
"name": "365DevNet",
|
||||||
|
"url": Astro.url.origin,
|
||||||
|
"potentialAction": {
|
||||||
|
"@type": "SearchAction",
|
||||||
|
"target": `${Astro.url.origin}/search?q={search_term_string}`,
|
||||||
|
"query-input": "required name=search_term_string"
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
|
||||||
<!-- Comment the line below to disable View Transitions -->
|
<!-- Comment the line below to disable View Transitions -->
|
||||||
<ClientRouter fallback="swap" />
|
<ClientRouter fallback="swap" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="antialiased text-default bg-page tracking-tight">
|
<body class="antialiased text-default bg-page tracking-tight">
|
||||||
|
<slot name="structured-data" />
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
<BasicScripts />
|
<BasicScripts />
|
||||||
|
@@ -15,6 +15,7 @@ const { metadata } = Astro.props;
|
|||||||
---
|
---
|
||||||
|
|
||||||
<Layout metadata={metadata}>
|
<Layout metadata={metadata}>
|
||||||
|
<slot name="structured-data" slot="structured-data" />
|
||||||
<slot name="header">
|
<slot name="header">
|
||||||
<Header {...headerData} isSticky showRssFeed showToggleTheme />
|
<Header {...headerData} isSticky showRssFeed showToggleTheme />
|
||||||
</slot>
|
</slot>
|
||||||
|
@@ -1,19 +1,16 @@
|
|||||||
---
|
---
|
||||||
|
export const prerender = false;
|
||||||
import Layout from '~/layouts/PageLayout.astro';
|
import Layout from '~/layouts/PageLayout.astro';
|
||||||
import Header from '~/components/widgets/Header.astro';
|
import Header from '~/components/widgets/Header.astro';
|
||||||
|
import StructuredData from '~/components/common/StructuredData.astro';
|
||||||
import Hero from '~/components/widgets/Hero.astro';
|
import Hero from '~/components/widgets/Hero.astro';
|
||||||
import Content from '~/components/widgets/Content.astro';
|
import Content from '~/components/widgets/Content.astro';
|
||||||
import Features3 from '~/components/widgets/Features3.astro';
|
import CompactSteps from '~/components/widgets/CompactSteps.astro';
|
||||||
import Testimonials from '~/components/widgets/Testimonials.astro';
|
import WorkExperience from '~/components/widgets/WorkExperience.astro';
|
||||||
import Steps from '~/components/widgets/Steps.astro';
|
import CompactCertifications from '~/components/widgets/CompactCertifications.astro';
|
||||||
|
import CompactSkills from '~/components/widgets/CompactSkills.astro';
|
||||||
import BlogLatestPosts from '~/components/widgets/BlogLatestPosts.astro';
|
import BlogLatestPosts from '~/components/widgets/BlogLatestPosts.astro';
|
||||||
import HomePageImage from '~/assets/images/richardbergsma.png'
|
import HomePageImage from '~/assets/images/richardbergsma.png'
|
||||||
import MicrosoftAssociate from '~/assets/images/microsoft-certified-associate-badge.webp'
|
|
||||||
import NexthinkAssociate from '~/assets/images/NexthinkAssociate.webp'
|
|
||||||
import NexthinkAdministrator from '~/assets/images/NexthinkAdministrator.webp'
|
|
||||||
import pcep from '~/assets/images/PCEP.webp'
|
|
||||||
import MicrosoftFundamentals from '~/assets/images/microsoft-certified-fundamentals-badge.webp'
|
|
||||||
import NexthinkAppExp from '~/assets/images/CertifiedNexthinkProfessionalinApplicationExperienceManagement.webp'
|
|
||||||
|
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import { getTranslation, supportedLanguages } from '~/i18n/translations';
|
import { getTranslation, supportedLanguages } from '~/i18n/translations';
|
||||||
@@ -60,6 +57,27 @@ const metadata = {
|
|||||||
|
|
||||||
<Layout metadata={metadata}>
|
<Layout metadata={metadata}>
|
||||||
<Fragment slot="announcement"></Fragment>
|
<Fragment slot="announcement"></Fragment>
|
||||||
|
|
||||||
|
<!-- Person Structured Data for SEO -->
|
||||||
|
<StructuredData slot="structured-data" data={{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Person",
|
||||||
|
"name": "Richard Bergsma",
|
||||||
|
"jobTitle": "IT Systems and Automation Manager",
|
||||||
|
"description": t.hero.subtitle,
|
||||||
|
"image": Astro.url.origin + "/src/assets/images/richardbergsma.png",
|
||||||
|
"url": Astro.url.origin,
|
||||||
|
"sameAs": [
|
||||||
|
"https://www.linkedin.com/in/rrpbergsma",
|
||||||
|
"https://github.com/rrpbergsma"
|
||||||
|
],
|
||||||
|
"knowsAbout": t.skills.items.map(skill => skill.title),
|
||||||
|
"worksFor": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "COFRA Holding C.V.",
|
||||||
|
"location": "Amsterdam"
|
||||||
|
}
|
||||||
|
}} />
|
||||||
<Fragment slot="header">
|
<Fragment slot="header">
|
||||||
<Header
|
<Header
|
||||||
links={[
|
links={[
|
||||||
@@ -68,6 +86,7 @@ const metadata = {
|
|||||||
{ text: t.navigation.resume, href: '#resume' },
|
{ text: t.navigation.resume, href: '#resume' },
|
||||||
{ text: t.navigation.certifications, href: '#certifications' },
|
{ text: t.navigation.certifications, href: '#certifications' },
|
||||||
{ text: t.navigation.skills, href: '#skills' },
|
{ text: t.navigation.skills, href: '#skills' },
|
||||||
|
{ text: t.navigation.education, href: '#education' },
|
||||||
{ text: t.navigation.blog, href: '#blog' },
|
{ text: t.navigation.blog, href: '#blog' },
|
||||||
]}
|
]}
|
||||||
isSticky
|
isSticky
|
||||||
@@ -115,7 +134,7 @@ const metadata = {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Fragment slot="content">
|
<Fragment slot="content">
|
||||||
<h2 class="text-3xl font-bold tracking-tight dark:text-white sm:text-4xl mb-2">{t.about.title}</h2>
|
<h2 class="text-3xl font-bold tracking-tight sm:text-4xl mb-2">{t.about.title}</h2>
|
||||||
{t.about.content.map((paragraph) => (
|
{t.about.content.map((paragraph) => (
|
||||||
<p>{paragraph}</p>
|
<p>{paragraph}</p>
|
||||||
<br />
|
<br />
|
||||||
@@ -127,67 +146,55 @@ const metadata = {
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
</Content>
|
</Content>
|
||||||
|
|
||||||
<!-- Steps Widget -->
|
<!-- Work Experience - Modern Timeline Layout -->
|
||||||
<Steps
|
<WorkExperience
|
||||||
id="resume"
|
id="resume"
|
||||||
title={t.resume.title}
|
title={t.resume.title}
|
||||||
|
compact={true}
|
||||||
items={t.resume.experience.map(exp => ({
|
items={t.resume.experience.map(exp => ({
|
||||||
title: `${exp.title}<br /> <span class="font-normal">${exp.company} - ${exp.location}</span> <br /> <span class="text-sm font-normal">${exp.period}</span>`,
|
title: exp.title,
|
||||||
|
company: exp.company,
|
||||||
|
date: exp.period,
|
||||||
|
location: exp.location,
|
||||||
description: exp.description,
|
description: exp.description,
|
||||||
icon: 'tabler:automation',
|
icon: 'tabler:briefcase',
|
||||||
}))}
|
}))}
|
||||||
classes={{ container: 'max-w-3xl' }}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Steps Widget -->
|
<!-- Certifications - Compact Layout -->
|
||||||
<Steps
|
<CompactCertifications
|
||||||
|
id="certifications"
|
||||||
|
title={t.certifications.title}
|
||||||
|
subtitle={t.certifications.subtitle}
|
||||||
|
testimonials={t.certifications.items.map((cert) => ({
|
||||||
|
name: cert.name,
|
||||||
|
issueDate: cert.issueDate,
|
||||||
|
description: cert.description,
|
||||||
|
linkUrl: cert.linkUrl,
|
||||||
|
image: cert.image
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Skills - Compact Layout -->
|
||||||
|
<CompactSkills
|
||||||
|
id="skills"
|
||||||
|
title={t.skills.title}
|
||||||
|
subtitle={t.skills.subtitle}
|
||||||
|
defaultIcon="tabler:point-filled"
|
||||||
|
items={t.skills.items.map(item => ({
|
||||||
|
title: item.title,
|
||||||
|
description: item.description,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Education - Compact Layout -->
|
||||||
|
<CompactSteps
|
||||||
id="education"
|
id="education"
|
||||||
title={t.education.title}
|
title={t.education.title}
|
||||||
items={t.education.items.map(item => ({
|
items={t.education.items.map(item => ({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
icon: 'tabler:school'
|
icon: 'tabler:school'
|
||||||
}))}
|
}))}
|
||||||
classes={{ container: 'max-w-3xl' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Testimonials Widget -->
|
|
||||||
<Testimonials
|
|
||||||
id="certifications"
|
|
||||||
title={t.certifications.title}
|
|
||||||
subtitle={t.certifications.subtitle}
|
|
||||||
testimonials={t.certifications.items.map((cert, index) => ({
|
|
||||||
name: cert.name,
|
|
||||||
issueDate: cert.issueDate,
|
|
||||||
description: cert.description,
|
|
||||||
linkUrl: cert.linkUrl,
|
|
||||||
image: {
|
|
||||||
src: [
|
|
||||||
NexthinkAppExp,
|
|
||||||
NexthinkAdministrator,
|
|
||||||
NexthinkAssociate,
|
|
||||||
pcep,
|
|
||||||
MicrosoftAssociate,
|
|
||||||
MicrosoftFundamentals,
|
|
||||||
MicrosoftAssociate,
|
|
||||||
MicrosoftFundamentals,
|
|
||||||
][index],
|
|
||||||
alt: cert.image.alt,
|
|
||||||
loading: 'lazy',
|
|
||||||
},
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Features3 Widget -->
|
|
||||||
<Features3
|
|
||||||
id="skills"
|
|
||||||
title={t.skills.title}
|
|
||||||
subtitle={t.skills.subtitle}
|
|
||||||
columns={3}
|
|
||||||
defaultIcon="tabler:point-filled"
|
|
||||||
items={t.skills.items.map(item => ({
|
|
||||||
title: item.title,
|
|
||||||
description: item.description,
|
|
||||||
}))}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- BlogLatestPost Widget -->
|
<!-- BlogLatestPost Widget -->
|
||||||
|
6
vendor/integration/index.ts
vendored
6
vendor/integration/index.ts
vendored
@@ -32,7 +32,11 @@ export default ({ config: _themeConfig = 'src/config.yaml' } = {}): AstroIntegra
|
|||||||
site: SITE.site,
|
site: SITE.site,
|
||||||
base: SITE.base,
|
base: SITE.base,
|
||||||
|
|
||||||
trailingSlash: SITE.trailingSlash ? 'always' : 'never',
|
trailingSlash: typeof SITE.trailingSlash === 'string'
|
||||||
|
? (SITE.trailingSlash === 'always' || SITE.trailingSlash === 'never' || SITE.trailingSlash === 'ignore'
|
||||||
|
? SITE.trailingSlash
|
||||||
|
: 'never')
|
||||||
|
: (SITE.trailingSlash ? 'always' : 'never'),
|
||||||
|
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
2
vendor/integration/utils/configBuilder.ts
vendored
2
vendor/integration/utils/configBuilder.ts
vendored
@@ -17,7 +17,7 @@ export interface SiteConfig {
|
|||||||
name: string;
|
name: string;
|
||||||
site?: string;
|
site?: string;
|
||||||
base?: string;
|
base?: string;
|
||||||
trailingSlash?: boolean;
|
trailingSlash?: boolean | string;
|
||||||
googleSiteVerificationId?: string;
|
googleSiteVerificationId?: string;
|
||||||
}
|
}
|
||||||
export interface MetaDataConfig extends Omit<MetaData, 'title'> {
|
export interface MetaDataConfig extends Omit<MetaData, 'title'> {
|
||||||
|
Reference in New Issue
Block a user