major update in looks and feel

This commit is contained in:
becarta
2025-03-06 00:03:16 +01:00
parent 3b98eae137
commit d66123c029
40 changed files with 1123 additions and 422 deletions

BIN
inter.zip Normal file

Binary file not shown.

View File

@@ -16,7 +16,7 @@ const {
SMTP_PORT = '587',
SMTP_USER = '',
SMTP_PASS = '',
ADMIN_EMAIL = 'richard@bergsma.it',
ADMIN_EMAIL = '',
WEBSITE_NAME = 'bergsma.it',
NODE_ENV = 'production'
} = process.env;

Binary file not shown.

View File

@@ -18,6 +18,12 @@
.text-muted {
color: var(--aw-color-text-muted);
}
.content-backdrop {
@apply bg-white/20 dark:bg-gray-900/15 backdrop-blur-sm hover:bg-white/30 hover:backdrop-blur-md dark:hover:backdrop-blur-md transition-all duration-300 ease-in-out;
}
.content-card {
@apply rounded-lg p-4 md:p-6 content-backdrop;
}
}
@layer components {

View File

@@ -20,11 +20,11 @@ const languages = [
const currentLanguage = languages.find(lang => lang.code === currentLang) || languages[0];
---
<div class="relative inline-block text-left">
<div class="relative inline-block text-left language-dropdown">
<div>
<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 focus-visible:ring-4 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 dropdown-button"
id="menu-button"
aria-expanded="false"
aria-haspopup="true"
@@ -42,7 +42,7 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
</div>
<div
class="hidden absolute left-0 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 dark:ring-gray-600 focus:outline-none transform opacity-0 scale-95 transition-all duration-200 max-h-[300px] overflow-y-auto w-full"
class="absolute left-0 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 dark:ring-gray-600 focus:outline-none transform opacity-0 scale-95 transition-all duration-200 max-h-[300px] overflow-y-auto w-full language-menu"
role="menu"
aria-orientation="vertical"
aria-labelledby="menu-button"
@@ -66,26 +66,52 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
))}
</div>
</div>
<select id="language-select" class="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 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 language-select">
{languages.map(lang => (
<option value={lang.code} selected={lang.code === currentLang}>
{lang.name}
</option>
))}
</select>
</div>
<style>
#language-menu.open-downward {
.language-dropdown {
@apply md:inline-block md:relative;
}
.language-select {
display: none;
}
@media (max-width: 767px) {
.language-dropdown {
display: none;
}
.language-select {
display: block;
}
}
.language-menu.open-downward {
top: 100%;
margin-top: 0.5rem;
transform-origin: top;
}
#language-menu.open-upward {
.language-menu.open-upward {
bottom: 100%;
margin-bottom: 0.5rem;
transform-origin: bottom;
}
#language-menu:not(.hidden).open-downward {
.language-menu:not(.hidden).open-downward {
animation: slideDown 0.2s ease-out forwards;
}
#language-menu:not(.hidden).open-upward {
.language-menu:not(.hidden).open-upward {
animation: slideUp 0.2s ease-out forwards;
}
@@ -110,14 +136,6 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
transform: scale(1) translateY(0);
}
}
@media (max-width: 767px) {
.absolute.right-0.w-56.rounded-md.shadow-lg.bg-white.dark\:bg-gray-800.ring-1.ring-black.ring-opacity-5.dark\:ring-gray-600.focus\:outline-none.transform.opacity-0.scale-95.transition-all.duration-200.max-h-\[300px\].overflow-y-auto {
width: auto;
min-width: 120px;
max-width: calc(100vw - 6rem);
}
}
</style>
<script define:vars={{ supportedLanguages }}>
@@ -127,8 +145,9 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
const chevronIcon = document.querySelector('#chevron-icon');
const selectedLanguageText = document.querySelector('#selected-language');
const languageButtons = document.querySelectorAll('[data-lang-code]');
const languageSelect = document.querySelector('#language-select');
if (!button || !menu || !chevronIcon || !selectedLanguageText) {
if (!button || !menu || !chevronIcon || !selectedLanguageText || !languageSelect) {
return;
}
@@ -280,41 +299,83 @@ const currentLanguage = languages.find(lang => lang.code === currentLang) || lan
// Construct the full URL
const newFullUrl = `${window.location.origin}${newUrl}`;
// Reload the page to ensure all content is updated to the new language
window.location.href = newFullUrl;
// Force a complete page reload to ensure all content is updated to the new language
// This bypasses any client-side caching and ensures a fresh server render
window.location.href = newFullUrl + '?t=' + Date.now();
});
});
// Close when clicking outside
document.addEventListener('click', (e) => {
const target = e.target;
if (isOpen && !menu.contains(target) && !button.contains(target)) {
closeMenu();
}
});
// Handle language selection from select element
languageSelect.addEventListener('change', (event) => {
const langCode = event.target.value;
if (!langCode) return;
// Handle keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && isOpen) {
closeMenu();
button.focus();
// Get current URL information
const currentUrl = new URL(window.location.href);
const currentPath = currentUrl.pathname.replace(/\/$/, '');
const currentHash = currentUrl.hash;
const pathSegments = currentPath.split('/').filter(Boolean);
// Check if we're on a language-specific path
const isLangPath = supportedLanguages.includes(pathSegments[0]);
// Get the previous language code
const previousLangCode = isLangPath ? pathSegments[0] : 'en';
// Extract the page path without language
let pagePath = '';
if (isLangPath && pathSegments.length > 1) {
// If we're on a language-specific path, get everything after the language code
pagePath = `/${pathSegments.slice(1).join('/')}`;
} else if (!isLangPath && pathSegments.length > 0) {
// If we're not on a language-specific path, use the current path
pagePath = `/${pathSegments.join('/')}`;
}
// 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);
// Handle special case for root path
const isRootPath = pathSegments.length === 0 || (isLangPath && pathSegments.length === 1);
let newIndex;
if (e.key === 'ArrowDown') {
newIndex = currentIndex < menuItems.length - 1 ? currentIndex + 1 : 0;
} else {
newIndex = currentIndex > 0 ? currentIndex - 1 : menuItems.length - 1;
// Construct the new URL
let newUrl = isRootPath ? `/${langCode}` : `/${langCode}${pagePath}`;
// Clean up any potential double slashes
newUrl = newUrl.replace(/\/+/g, '/');
// Append hash fragment if it exists
if (currentHash) {
newUrl += currentHash;
}
// Store the language preference in localStorage and cookies
if (window.languageUtils) {
window.languageUtils.storeLanguagePreference(langCode);
} else {
// Fallback if languageUtils is not available
localStorage.setItem('preferredLanguage', langCode);
// Also set a cookie for server-side detection
const expirationDate = new Date();
expirationDate.setFullYear(expirationDate.getFullYear() + 1);
document.cookie = `preferredLanguage=${langCode}; expires=${expirationDate.toUTCString()}; path=/; SameSite=Lax`;
}
// Dispatch the language changed event
const reloadEvent = new CustomEvent('languageChanged', {
detail: {
langCode,
previousLangCode,
path: newUrl,
willReload: true
}
});
document.dispatchEvent(reloadEvent);
menuItems[newIndex].focus();
}
// Construct the full URL
const newFullUrl = `${window.location.origin}${newUrl}`;
// Force a complete page reload to ensure all content is updated to the new language
// This bypasses any client-side caching and ensures a fresh server render
window.location.href = newFullUrl + '?t=' + Date.now();
});
}

View File

@@ -1,7 +1,5 @@
---
import { UI } from 'astrowind:config';
// TODO: This code is temporary
---
<script is:inline define:vars={{ defaultTheme: UI.theme || 'system' }}>

View File

@@ -42,7 +42,7 @@ import { UI } from 'astrowind:config';
const onLoad = function () {
let lastKnownScrollPosition = window.scrollY;
let ticking = true;
let ticking = false;
attachEvent('#header nav', 'click', function () {
document.querySelector('[data-aw-toggle-menu]')?.classList.remove('expanded');
@@ -239,7 +239,6 @@ import { UI } from 'astrowind:config';
// Store language preference in localStorage
function storeLanguagePreference(langCode) {
localStorage.setItem('preferredLanguage', langCode);
console.log('Language preference stored:', langCode);
}
// Function to update URLs with the current language
@@ -301,11 +300,16 @@ import { UI } from 'astrowind:config';
// Store the selected language in localStorage
if (event.detail && event.detail.langCode) {
storeLanguagePreference(event.detail.langCode);
console.log('Language changed:', event.detail);
// Always update all internal links with the new language
// regardless of whether we're doing a full page reload
updateUrlsWithLanguage();
// If this event includes a path but doesn't indicate it will reload,
// force a full page reload to ensure all content is properly translated
if (event.detail.path && !event.detail.willReload) {
window.location.replace(event.detail.path);
}
}
});
@@ -313,10 +317,7 @@ import { UI } from 'astrowind:config';
updateUrlsWithLanguage();
// Process links after client-side navigation
document.addEventListener('astro:page-load', () => {
// Short delay to ensure DOM is fully updated
setTimeout(updateUrlsWithLanguage, 0);
});
document.addEventListener('astro:page-load', updateUrlsWithLanguage);
// Also update links when the DOM content is loaded
document.addEventListener('DOMContentLoaded', updateUrlsWithLanguage);
@@ -472,19 +473,15 @@ import { UI } from 'astrowind:config';
// Form validation and submission
contactForm.addEventListener('submit', async function(e) {
e.preventDefault();
console.log('Form submitted');
// Reset previous error messages
resetFormErrors();
// Client-side validation
if (!validateForm(contactForm)) {
console.log('Form validation failed');
return;
}
console.log('Form validation passed');
// Show loading state
const submitButton = contactForm.querySelector('button[type="submit"]');
const originalButtonText = submitButton.innerHTML;
@@ -494,37 +491,24 @@ import { UI } from 'astrowind:config';
try {
const formData = new FormData(contactForm);
// Log form data
console.log('Form data:');
for (const [key, value] of formData.entries()) {
console.log(`${key}: ${value}`);
}
// Add timestamp to help prevent duplicate submissions
formData.append('timestamp', Date.now().toString());
// Submit the form to Netlify
console.log('Submitting form to Netlify');
const response = await fetch('/', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(formData).toString()
});
console.log('Response status:', response.status);
if (response.ok) {
console.log('Form submission successful');
// Show success message
document.getElementById('form-success').classList.remove('hidden');
contactForm.reset();
} else {
console.log('Form submission failed');
document.getElementById('form-error').classList.remove('hidden');
}
} catch (error) {
console.error('Error submitting form:', error);
console.error('Error details:', error.message);
} catch {
document.getElementById('form-error').classList.remove('hidden');
} finally {
// Restore button state

View File

@@ -0,0 +1,85 @@
---
interface Props {
items: {
title: string;
description: string;
}[];
}
const { items } = Astro.props;
---
<div class="accordion">
{items.map((item, index) => (
<div class="accordion-item">
<button class="accordion-header" data-accordion-target={`#accordion-body-${index}`}>
{item.title}
<svg class="w-4 h-4 ml-2 shrink-0" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</button>
<div id={`accordion-body-${index}`} class="accordion-body" aria-labelledby={`accordion-header-${index}`}>
<div class="p-5 border border-b-0 dark:border-gray-700 dark:bg-gray-900">
<p class="text-gray-500 dark:text-gray-400">{item.description}</p>
</div>
</div>
</div>
))}
</div>
<script>
const accordionHeaders = document.querySelectorAll('.accordion-header');
accordionHeaders.forEach(header => {
header.addEventListener('click', () => {
const target = header.getAttribute('data-accordion-target');
const body = document.querySelector(target);
if (body) {
const expanded = (body as HTMLElement).classList.contains('expanded');
// Close all accordion items
document.querySelectorAll('.accordion-body.expanded').forEach(item => {
if (item && 'style' in item) {
(item as any).style.display = 'none';
item.classList.remove('expanded');
}
});
// Open the clicked accordion item if it was closed
if (!expanded) {
body.classList.add('expanded');
if (body && 'style' in body) {
(body as HTMLElement).style.display = 'block';
}
}
}
});
});
</script>
<style>
.accordion-header {
display: flex;
align-items: center;
width: 100%;
padding: 1rem;
font-size: 1.125rem;
line-height: 1.75rem;
font-weight: 500;
text-align: left;
border-bottom: 1px solid #e5e7eb;
cursor: pointer;
transition: background-color 0.15s ease;
}
.accordion-header:hover {
background-color: #f9fafb;
}
.accordion-body {
display: none;
}
.accordion-body.expanded {
display: block;
}
</style>

View File

@@ -1,11 +1,302 @@
---
import { Icon } from 'astro-icon/components';
export interface Props {
isDark?: boolean;
showIcons?: boolean;
disableParallax?: boolean;
}
const { isDark = false } = Astro.props;
---
const { isDark = false, showIcons = true, disableParallax = false } = Astro.props;
<div class:list={['absolute inset-0', { 'bg-dark dark:bg-transparent': isDark }]}>
// Define color palettes for light and dark modes with higher contrast
const lightModeColors = [
'text-blue-600/40',
'text-indigo-600/40',
'text-purple-600/40',
'text-cyan-600/40',
'text-teal-600/40',
'text-emerald-600/40',
'text-sky-600/40',
'text-violet-600/40',
];
const darkModeColors = [
'dark:text-blue-400/45',
'dark:text-indigo-400/45',
'dark:text-purple-400/45',
'dark:text-cyan-400/45',
'dark:text-teal-400/45',
'dark:text-emerald-400/45',
'dark:text-sky-400/45',
'dark:text-violet-400/45',
];
// Define interfaces for our icon objects
interface BaseIconObject {
icon: string;
x: string;
y: string;
size: string;
opacity: string;
rotate: string;
}
interface IconWithColors extends BaseIconObject {
lightColor: string;
darkColor: string;
}
// Function to get a random color from the palette
const getRandomColor = (palette: string[]): string => {
const randomIndex = Math.floor(Math.random() * palette.length);
return palette[randomIndex];
};
// List of icons to be used in the background
// Expanded to include a wider variety of symbols for more visual diversity
const iconNames: string[] = [
// Core IT/Tech icons
'tabler:settings-automation',
'tabler:brand-office',
'tabler:api',
'tabler:server',
'tabler:message-chatbot',
'tabler:share',
'tabler:code',
'tabler:cloud',
'tabler:device-laptop',
'tabler:chart-line',
'tabler:database',
'tabler:brand-github',
'tabler:device-desktop',
'tabler:brand-azure',
// Additional tech-related icons
'tabler:cpu',
'tabler:device-mobile',
'tabler:wifi',
'tabler:network',
'tabler:shield',
'tabler:lock',
'tabler:key',
'tabler:rocket',
// Tangentially related icons for visual diversity
'tabler:bulb',
'tabler:compass',
'tabler:binary',
'tabler:infinity',
'tabler:brain',
];
// Function to get a random value within a range
const getRandomInRange = (min: number, max: number): number => {
return Math.random() * (max - min) + min;
};
// Function to get a random rotation with increased range
const getRandomRotation = (): string => {
return `${getRandomInRange(-30, 30)}deg`;
};
// Function to get a random size (restored to original dimensions)
const getRandomSize = (): string => {
return `${getRandomInRange(140, 180)}px`;
};
// Function to get a random opacity
const getRandomOpacity = (): string => {
return getRandomInRange(0.32, 0.38).toFixed(2);
};
// Create a spacious layout with well-separated icons
const createSpacedIcons = (): BaseIconObject[] => {
const icons: BaseIconObject[] = [];
const rows = 6; // Reduced from 8 to 6 for fewer potential positions
const cols = 6; // Reduced from 8 to 6 for fewer potential positions
// Define larger margins to keep icons away from edges (in percentage)
const marginX = 10; // 10% margin from left and right edges (increased from 5%)
const marginY = 10; // 10% margin from top and bottom edges (increased from 5%)
// Minimum distance between icons (in percentage points)
const minDistance = 20; // Ensure at least 20% distance between any two icons
// Create a base grid of positions
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
// Randomly skip more positions (75% chance) for fewer icons overall
if (Math.random() < 0.75) {
continue;
}
// Base position in the grid with margins applied
const baseX = marginX + ((col / cols) * (100 - 2 * marginX));
const baseY = marginY + ((row / rows) * (100 - 2 * marginY));
// Add limited randomness to the position (±5%) to maintain spacing
const randomOffsetX = getRandomInRange(-5, 5);
const randomOffsetY = getRandomInRange(-5, 5);
// Ensure positions stay within margins
const x = Math.max(marginX, Math.min(100 - marginX, baseX + randomOffsetX));
const y = Math.max(marginY, Math.min(100 - marginY, baseY + randomOffsetY));
// Check if this position is too close to any existing icon
let tooClose = false;
for (const existingIcon of icons) {
// Extract numeric values from percentage strings
const existingX = parseFloat(existingIcon.x);
const existingY = parseFloat(existingIcon.y);
// Calculate distance between points
const distance = Math.sqrt(
Math.pow(x - existingX, 2) +
Math.pow(y - existingY, 2)
);
// If too close to an existing icon, skip this position
if (distance < minDistance) {
tooClose = true;
break;
}
}
if (tooClose) {
continue;
}
// Randomly select an icon from the expanded set
const iconIndex = Math.floor(Math.random() * iconNames.length);
icons.push({
icon: iconNames[iconIndex],
x: `${x}%`,
y: `${y}%`,
size: getRandomSize(),
opacity: getRandomOpacity(),
rotate: getRandomRotation(),
});
}
}
return icons;
};
const icons: BaseIconObject[] = createSpacedIcons();
// Assign random colors to each icon
const iconsWithColors: IconWithColors[] = icons.map(icon => ({
icon: icon.icon,
x: icon.x,
y: icon.y,
size: icon.size,
opacity: icon.opacity,
rotate: icon.rotate,
lightColor: getRandomColor(lightModeColors),
darkColor: getRandomColor(darkModeColors),
}));
---
<div class:list={['absolute inset-0', { 'backdrop-blur-sm bg-white/5 dark:bg-gray-900/10': isDark }]}>
<slot />
{showIcons && (
/* Decorative background icons with parallax effect */
<div id="parallax-background" class="absolute inset-0 overflow-hidden pointer-events-none z-[-5]">
{iconsWithColors.map(({ icon, x, y, size, opacity, rotate, lightColor, darkColor }, index) => (
<div
class={`absolute ${lightColor} ${darkColor} parallax-icon`}
style={`left: ${x}; top: ${y}; opacity: ${opacity}; transform: rotate(${rotate}); will-change: transform; transition: transform 0.1s ease-out;`}
data-depth={`${0.5 + (index % 3) * 0.2}`}
data-initial-x={x}
data-initial-y={y}
>
<Icon name={icon} style={`width: ${size}; height: ${size};`} />
</div>
))}
</div>
)}
</div>
{showIcons && (
<script define:vars={{ disableParallax }}>
// Parallax scrolling effect for background icons
document.addEventListener('DOMContentLoaded', () => {
// Get all parallax icons
const parallaxIcons = document.querySelectorAll('.parallax-icon');
// Skip parallax on mobile devices for better performance or if parallax is disabled
const isMobile = window.matchMedia('(max-width: 768px)').matches;
if (isMobile || disableParallax) return;
// Variables to track scroll position
let lastScrollY = window.scrollY;
let ticking = false;
// Function to update icon positions based on scroll
const updateParallax = () => {
parallaxIcons.forEach((icon) => {
const depth = parseFloat(icon.getAttribute('data-depth') || '0.5');
// Calculate parallax offset based on scroll position and depth
// Lower depth value means the icon moves slower (appears further away)
const yOffset = (lastScrollY * depth * 0.15);
// Get the original rotation
const transformValue = icon.style.transform;
const rotateMatch = transformValue.match(/rotate\([^)]+\)/);
const rotateValue = rotateMatch ? rotateMatch[0] : 'rotate(0deg)';
// Apply transform with the original rotation plus the parallax offset
icon.style.transform = `${rotateValue} translate3d(0, ${yOffset}px, 0)`;
});
ticking = false;
};
// Throttle scroll events for better performance
const onScroll = () => {
lastScrollY = window.scrollY;
if (!ticking) {
window.requestAnimationFrame(() => {
updateParallax();
ticking = false;
});
ticking = true;
}
};
// Add scroll event listener
window.addEventListener('scroll', onScroll, { passive: true });
// Update on resize (debounced)
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
// Check if device is now mobile and disable parallax if needed
const isMobileNow = window.matchMedia('(max-width: 768px)').matches;
if (isMobileNow) {
// Reset positions on mobile
parallaxIcons.forEach((icon) => {
const transformValue = icon.style.transform;
const rotateMatch = transformValue.match(/rotate\([^)]+\)/);
const rotateValue = rotateMatch ? rotateMatch[0] : 'rotate(0deg)';
icon.style.transform = rotateValue;
});
} else {
// Update parallax on desktop
updateParallax();
}
}, 200);
}, { passive: true });
// Initial update
updateParallax();
});
</script>
)}

View File

@@ -16,7 +16,7 @@ const {
panel: panelClass = '',
title: titleClass = '',
description: descriptionClass = '',
icon: defaultIconClass = 'text-primary dark:text-slate-200 border-primary dark:border-blue-700',
icon: defaultIconClass = 'text-primary dark:text-blue-300 border-primary dark:border-blue-500 dark:shadow-blue-500/40 dark:shadow-sm',
} = classes;
---
@@ -26,7 +26,7 @@ const {
{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',
'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 backdrop-blur-sm bg-white/15 dark:bg-transparent hover:bg-white/30 hover:backdrop-blur-md dark:hover:backdrop-blur-md hover:translate-y-[-2px]',
panelClass,
itemClasses?.panel
)}

View File

@@ -15,7 +15,7 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '' }
}
</style>
<form id="contact-form" name="contact" netlify method="POST" class="needs-validation" data-netlify="true" data-netlify-honeypot="bot-field" novalidate>
<form id="contact-form" name="contact" method="POST" class="needs-validation" data-netlify="true" data-netlify-honeypot="bot-field" novalidate>
<!-- Form status messages -->
<div id="form-success" class="hidden mb-6 p-4 bg-green-100 border border-green-200 text-green-700 rounded-lg">

View File

@@ -0,0 +1,305 @@
---
import { Icon } from 'astro-icon/components';
export interface Props {
isDark?: boolean;
}
const { isDark = false } = Astro.props;
// Define color palettes for light and dark modes with higher contrast
const lightModeColors = [
'text-blue-600/40',
'text-indigo-600/40',
'text-purple-600/40',
'text-cyan-600/40',
'text-teal-600/40',
'text-emerald-600/40',
'text-sky-600/40',
'text-violet-600/40',
];
const darkModeColors = [
'dark:text-blue-400/45',
'dark:text-indigo-400/45',
'dark:text-purple-400/45',
'dark:text-cyan-400/45',
'dark:text-teal-400/45',
'dark:text-emerald-400/45',
'dark:text-sky-400/45',
'dark:text-violet-400/45',
];
// Define interfaces for our icon objects
interface BaseIconObject {
icon: string;
x: string;
y: string;
size: string;
opacity: string;
rotate: string;
}
interface IconWithColors extends BaseIconObject {
lightColor: string;
darkColor: string;
}
// Function to get a random color from the palette
const getRandomColor = (palette: string[]): string => {
const randomIndex = Math.floor(Math.random() * palette.length);
return palette[randomIndex];
};
// List of icons to be used in the background
// Expanded to include a wider variety of symbols for more visual diversity
const iconNames: string[] = [
// Core IT/Tech icons
'tabler:settings-automation',
'tabler:brand-office',
'tabler:api',
'tabler:server',
'tabler:message-chatbot',
'tabler:share',
'tabler:code',
'tabler:cloud',
'tabler:device-laptop',
'tabler:chart-line',
'tabler:database',
'tabler:brand-github',
'tabler:device-desktop',
'tabler:brand-azure',
// Additional tech-related icons
'tabler:cpu',
'tabler:device-mobile',
'tabler:wifi',
'tabler:network',
'tabler:shield',
'tabler:lock',
'tabler:key',
'tabler:rocket',
'tabler:satellite-off',
// Tangentially related icons for visual diversity
'tabler:bulb',
'tabler:puzzle',
'tabler:compass',
'tabler:chart-dots',
'tabler:math',
'tabler:atom',
'tabler:binary',
'tabler:circuit-resistor',
'tabler:infinity',
'tabler:planet',
'tabler:brain',
'tabler:cube',
];
// Function to get a random value within a range
const getRandomInRange = (min: number, max: number): number => {
return Math.random() * (max - min) + min;
};
// Function to get a random rotation with increased range
const getRandomRotation = (): string => {
return `${getRandomInRange(-30, 30)}deg`;
};
// Function to get a random size
const getRandomSize = (): string => {
return `${getRandomInRange(140, 180)}px`;
};
// Function to get a random opacity
const getRandomOpacity = (): string => {
return getRandomInRange(0.32, 0.38).toFixed(2);
};
// Create a spacious layout with well-separated icons
const createSpacedIcons = (): BaseIconObject[] => {
const icons: BaseIconObject[] = [];
const rows = 10; // Increased from 6 to 10 for more coverage across the entire page
const cols = 6;
// Define larger margins to keep icons away from edges (in percentage)
const marginX = 10;
const marginY = 5; // Reduced from 10 to 5 to allow icons to span more of the page height
// Minimum distance between icons (in percentage points)
const minDistance = 15; // Reduced from 20 to 15 to allow more icons
// Create a base grid of positions
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
// Randomly skip positions (65% chance) for fewer icons overall
if (Math.random() < 0.65) {
continue;
}
// Base position in the grid with margins applied
const baseX = marginX + ((col / cols) * (100 - 2 * marginX));
const baseY = marginY + ((row / rows) * (100 - 2 * marginY));
// Add limited randomness to the position (±5%) to maintain spacing
const randomOffsetX = getRandomInRange(-5, 5);
const randomOffsetY = getRandomInRange(-5, 5);
// Ensure positions stay within margins
const x = Math.max(marginX, Math.min(100 - marginX, baseX + randomOffsetX));
const y = Math.max(marginY, Math.min(100 - marginY, baseY + randomOffsetY));
// Check if this position is too close to any existing icon
let tooClose = false;
for (const existingIcon of icons) {
// Extract numeric values from percentage strings
const existingX = parseFloat(existingIcon.x);
const existingY = parseFloat(existingIcon.y);
// Calculate distance between points
const distance = Math.sqrt(
Math.pow(x - existingX, 2) +
Math.pow(y - existingY, 2)
);
// If too close to an existing icon, skip this position
if (distance < minDistance) {
tooClose = true;
break;
}
}
if (tooClose) {
continue;
}
// Randomly select an icon from the expanded set
const iconIndex = Math.floor(Math.random() * iconNames.length);
icons.push({
icon: iconNames[iconIndex],
x: `${x}%`,
y: `${y}%`,
size: getRandomSize(),
opacity: getRandomOpacity(),
rotate: getRandomRotation(),
});
}
}
return icons;
};
const icons: BaseIconObject[] = createSpacedIcons();
// Assign random colors to each icon
const iconsWithColors: IconWithColors[] = icons.map(icon => ({
icon: icon.icon,
x: icon.x,
y: icon.y,
size: icon.size,
opacity: icon.opacity,
rotate: icon.rotate,
lightColor: getRandomColor(lightModeColors),
darkColor: getRandomColor(darkModeColors),
}));
---
<div class="fixed inset-0 overflow-hidden pointer-events-none z-[-5]" aria-hidden="true">
<div class:list={['absolute inset-0', { 'backdrop-blur-sm bg-white/5 dark:bg-gray-900/10': isDark }]}></div>
{/* Decorative background icons with parallax effect */}
<div id="parallax-background" class="absolute inset-0 overflow-hidden">
{iconsWithColors.map(({ icon, x, y, size, opacity, rotate, lightColor, darkColor }, index) => (
<div
class={`absolute ${lightColor} ${darkColor} parallax-icon`}
style={`left: ${x}; top: ${y}; opacity: ${opacity}; transform: rotate(${rotate}); will-change: transform; transition: transform 0.1s ease-out;`}
data-depth={`${0.5 + (index % 3) * 0.2}`}
data-initial-x={x}
data-initial-y={y}
>
<Icon name={icon} style={`width: ${size}; height: ${size};`} />
</div>
))}
</div>
</div>
<script>
// Parallax scrolling effect for background icons
document.addEventListener('DOMContentLoaded', () => {
// Get all parallax icons
const parallaxIcons = document.querySelectorAll<HTMLElement>('.parallax-icon');
// Skip parallax on mobile devices for better performance
const isMobile = window.matchMedia('(max-width: 768px)').matches;
if (isMobile) return;
// Variables to track scroll position
let lastScrollY = window.scrollY;
let ticking = false;
// Function to update icon positions based on scroll
const updateParallax = () => {
parallaxIcons.forEach((icon) => {
const depth = parseFloat(icon.getAttribute('data-depth') || '0.5');
// Calculate parallax offset based on scroll position and depth
// Lower depth value means the icon moves slower (appears further away)
const yOffset = (lastScrollY * depth * 0.15);
// Get the original rotation
const transformValue = icon.style.transform;
const rotateMatch = transformValue.match(/rotate\([^)]+\)/);
const rotateValue = rotateMatch ? rotateMatch[0] : 'rotate(0deg)';
// Apply transform with the original rotation plus the parallax offset
icon.style.transform = `${rotateValue} translate3d(0, ${yOffset}px, 0)`;
});
ticking = false;
};
// Throttle scroll events for better performance
const onScroll = () => {
lastScrollY = window.scrollY;
if (!ticking) {
window.requestAnimationFrame(() => {
updateParallax();
ticking = false;
});
ticking = true;
}
};
// Add scroll event listener
window.addEventListener('scroll', onScroll, { passive: true });
// Update on resize (debounced)
let resizeTimer: number;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
// Check if device is now mobile and disable parallax if needed
const isMobileNow = window.matchMedia('(max-width: 768px)').matches;
if (isMobileNow) {
// Reset positions on mobile
parallaxIcons.forEach((icon) => {
const transformValue = icon.style.transform;
const rotateMatch = transformValue.match(/rotate\([^)]+\)/);
const rotateValue = rotateMatch ? rotateMatch[0] : 'rotate(0deg)';
icon.style.transform = rotateValue;
});
} else {
// Update parallax on desktop
updateParallax();
}
}, 200) as unknown as number;
}, { passive: true });
// Initial update
updateParallax();
});
</script>

View File

@@ -20,16 +20,16 @@ const {
(title || subtitle || tagline) && (
<div class={twMerge('mb-8 md:mx-auto md:mb-12 text-center', containerClass)}>
{tagline && (
<p class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase" set:html={tagline} />
<p class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase content-backdrop p-2 inline-block rounded-md" set:html={tagline} />
)}
{title && (
<h2
class={twMerge('font-bold leading-tighter tracking-tighter font-heading text-heading text-3xl', titleClass)}
class={twMerge('font-bold leading-tighter tracking-tighter font-heading text-heading text-3xl content-backdrop p-2 block rounded-md', titleClass)}
set:html={title}
/>
)}
{subtitle && <p class={twMerge('mt-4 text-muted', subtitleClass)} set:html={subtitle} />}
{subtitle && <p class={twMerge('mt-2 text-muted content-backdrop p-2 rounded-md', subtitleClass)} set:html={subtitle} />}
</div>
)
}

View File

@@ -11,7 +11,7 @@ const {
panel: panelClass = '',
title: titleClass = '',
description: descriptionClass = '',
icon: defaultIconClass = 'text-primary',
icon: defaultIconClass = 'text-primary dark:text-blue-300 dark:shadow-blue-500/40 dark:shadow-sm',
action: actionClass = '',
} = classes;
---
@@ -34,7 +34,7 @@ const {
>
{items.map(({ title, description, icon, callToAction, classes: itemClasses = {} }) => (
<div class="intersect-once motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade">
<div class={twMerge('flex flex-row max-w-md', panelClass, itemClasses?.panel)}>
<div class={twMerge('flex flex-row max-w-md backdrop-blur-sm bg-white/15 dark:bg-transparent p-3 rounded-md border border-gray-200 dark:border-gray-700 hover:bg-white/30 hover:backdrop-blur-md dark:hover:backdrop-blur-md transition-all duration-300 ease-in-out', panelClass, itemClasses?.panel)}>
<div class="flex justify-center">
{(icon || defaultIcon) && (
<Icon

View File

@@ -17,7 +17,7 @@ const {
panel: panelClass = '',
title: titleClass = '',
description: descriptionClass = '',
icon: defaultIconClass = 'text-primary dark:text-slate-200 border-primary dark:border-blue-700',
icon: defaultIconClass = 'text-primary dark:text-blue-300 border-primary dark:border-blue-500 dark:shadow-blue-500/40 dark:shadow-sm',
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',
@@ -28,7 +28,7 @@ const {
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="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 shadow-sm bg-primary/70 dark:bg-blue-700/30" class:list={[timelineClass]}></div>
<div class="relative">
{items.map((item, index) => {
@@ -69,7 +69,7 @@ const {
>
<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`,
`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 backdrop-blur-sm bg-white/15 dark:bg-transparent hover:bg-white/30 hover:backdrop-blur-md dark:hover:backdrop-blur-md hover:translate-y-[-3px] group card-container`,
panelClass,
itemClasses?.panel
)}
@@ -95,12 +95,11 @@ const {
</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'
'from-transparent to-primary/50 dark:to-blue-700'
)}></div>
</div>
</div>

View File

@@ -16,7 +16,7 @@ const {
panel: panelClass = '',
title: titleClass = '',
description: descriptionClass = '',
icon: defaultIconClass = 'text-primary dark:text-slate-200 border-primary dark:border-blue-700',
icon: defaultIconClass = 'text-primary dark:text-blue-300 border-primary dark:border-blue-500 dark:shadow-blue-500/40 dark:shadow-sm',
arrow: arrowClass = 'text-primary dark:text-slate-200',
} = classes;
---
@@ -68,7 +68,7 @@ const {
{/* 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',
'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 backdrop-blur-sm bg-white/15 dark:bg-transparent hover:bg-white/30 hover:backdrop-blur-md dark:hover:backdrop-blur-md ml-8 md:ml-0 shadow-sm relative hover:translate-y-[-2px]',
panelClass,
itemClasses?.panel
)}

View File

@@ -16,7 +16,7 @@ const {
panel: panelClass = '',
title: titleClass = '',
description: descriptionClass = '',
icon: defaultIconClass = 'text-primary dark:text-slate-200 border-primary dark:border-blue-700',
icon: defaultIconClass = 'text-primary dark:text-blue-300 border-primary dark:border-blue-500 dark:shadow-blue-500/40 dark:shadow-sm',
} = classes;
---
@@ -42,7 +42,7 @@ const {
)}
</div>
</div>
{index !== items.length - 1 && <div class="w-px h-full bg-black/10 dark:bg-slate-400/50" />}
{index !== items.length - 1 && <div class="w-px h-full bg-primary/30 dark:bg-slate-400/50" />}
</div>
<div class={`pt-1 ${index !== items.length - 1 ? 'pb-8' : ''}`}>
{title && <p class={twMerge('text-xl font-bold', titleClass, itemClasses?.title)} set:html={title} />}

View File

@@ -7,19 +7,22 @@ import Background from './Background.astro';
export interface Props extends Widget {
containerClass?: string;
['as']?: HTMLTag;
disableBackground?: boolean;
}
const { id, isDark = false, containerClass = '', bg, as = 'section' } = Astro.props;
const { id, isDark = false, containerClass = '', bg, as = 'section', disableBackground = true } = Astro.props;
const WrapperTag = as;
---
<WrapperTag class="relative not-prose scroll-mt-[72px]" {...id ? { id } : {}}>
<div class="absolute inset-0 pointer-events-none -z-[1]" aria-hidden="true">
<slot name="bg">
{bg ? <Fragment set:html={bg} /> : <Background isDark={isDark} />}
</slot>
</div>
{!disableBackground && (
<div class="absolute inset-0 pointer-events-none -z-[1]" aria-hidden="true">
<slot name="bg">
{bg ? <Fragment set:html={bg} /> : <Background isDark={isDark} />}
</slot>
</div>
)}
<div
class:list={[
twMerge(

View File

@@ -27,7 +27,7 @@ const {
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-6xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
<div
class="max-w-3xl mx-auto text-center p-6 rounded-md shadow-xl dark:shadow-none dark:border dark:border-slate-600"
class="max-w-3xl mx-auto text-center p-6 rounded-md shadow-md backdrop-blur-sm bg-white/15 dark:bg-transparent border border-gray-200 dark:border-slate-600 hover:bg-white/25 dark:hover:backdrop-blur-md transition-all duration-300 ease-in-out hover:shadow-lg"
>
<Headline
title={title}

View File

@@ -48,7 +48,7 @@ const {
{
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 flex-col p-3 rounded-md shadow-md dark:shadow-none border border-gray-200 dark:border-slate-600 backdrop-blur-sm bg-white/15 dark:bg-transparent hover:shadow-lg transition-all duration-300 ease-in-out hover:bg-white/30 hover:backdrop-blur-md dark:hover:backdrop-blur-md hover:translate-y-[-2px]">
<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"

View File

@@ -31,9 +31,9 @@ const {
<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="group backdrop-blur-sm bg-white/15 dark:bg-transparent p-3 rounded-lg shadow-sm hover:shadow-md transition-all duration-300 ease-in-out hover:bg-white/30 hover:backdrop-blur-md dark:hover:backdrop-blur-md border border-gray-200 dark:border-gray-700 hover:translate-y-[-2px]">
<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" />
<Icon name={icon || defaultIcon} class="w-5 h-5 mr-2 text-primary dark:text-blue-300 transition-transform duration-300 group-hover:scale-110 dark:shadow-blue-500/40 dark:shadow-sm" />
<h3 class="text-base font-semibold">{title}</h3>
</div>
<div class="text-muted overflow-hidden">

View File

@@ -26,7 +26,7 @@ const {
{
inputs && (
<div class="flex flex-col max-w-xl mx-auto rounded-lg backdrop-blur border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900 shadow p-4 sm:p-6 lg:p-8 w-full">
<div class="flex flex-col max-w-xl mx-auto rounded-lg backdrop-blur-sm bg-white/15 dark:bg-transparent border border-gray-200 dark:border-gray-700 shadow-md hover:shadow-lg hover:bg-white/25 dark:hover:backdrop-blur-md transition-all duration-300 ease-in-out p-4 sm:p-6 lg:p-8 w-full">
<FormContainer
inputs={inputs}
textarea={textarea}

View File

@@ -44,7 +44,7 @@ const {
<div class="mx-auto max-w-7xl p-4 md:px-8">
<div class={`md:flex ${isReversed ? 'md:flex-row-reverse' : ''} md:gap-16`}>
<div class="md:basis-1/2 self-center">
{content && <div class="mb-12 text-lg dark:text-slate-400" set:html={content} />}
{content && <div class="mb-12 text-lg dark:text-slate-400 content-backdrop p-3 rounded-md" set:html={content} />}
{
callToAction && (
@@ -60,7 +60,7 @@ const {
defaultIcon="tabler:check"
classes={{
container: `gap-y-4 md:gap-y-8`,
panel: 'max-w-none',
panel: 'max-w-none content-backdrop p-3 rounded-md',
title: 'text-lg font-medium leading-6 dark:text-white ml-2 rtl:ml-0 rtl:mr-2',
description: 'text-muted dark:text-slate-400 ml-2 rtl:ml-0 rtl:mr-2',
icon: 'flex h-7 w-7 items-center justify-center rounded-full bg-green-600 dark:bg-green-700 text-gray-50 p-1',

View File

@@ -1,6 +1,6 @@
---
import Headline from '~/components/ui/Headline.astro';
import ItemGrid from '~/components/ui/ItemGrid.astro';
import Accordion from '~/components/ui/Accordion.astro';
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
import type { Faqs as Props } from '~/types';
@@ -20,14 +20,5 @@ const {
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-7xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
<Headline title={title} subtitle={subtitle} tagline={tagline} />
<ItemGrid
items={items}
columns={columns}
defaultIcon="tabler:chevron-right"
classes={{
container: `${columns === 1 ? 'max-w-4xl' : ''} gap-y-8 md:gap-y-12`,
panel: 'max-w-none',
icon: 'flex-shrink-0 mt-1 w-6 h-6 text-primary',
}}
/>
<Accordion items={items} />
</WidgetWrapper>

View File

@@ -63,8 +63,9 @@ if (!currentLang) {
}
}
// Get translated header data
// Get translated header data - ensure we're using the current language
const headerData = getHeaderData(currentLang);
console.log(`Header initialized with language: ${currentLang}`);
const {
id = 'header',

View File

@@ -1,8 +1,13 @@
---
import Image from '~/components/common/Image.astro';
import Button from '~/components/ui/Button.astro';
import Background from '~/components/ui/Background.astro';
import type { Hero as Props } from '~/types';
import type { Hero } from '~/types';
export interface Props extends Hero {
disableBackground?: boolean;
}
const {
title = await Astro.slots.render('title'),
@@ -14,16 +19,20 @@ const {
image = await Astro.slots.render('image'),
id,
isDark = false,
bg = await Astro.slots.render('bg'),
disableBackground = true,
} = Astro.props;
---
<section class="relative md:-mt-[76px] not-prose bg-hero" {...id ? { id } : {}}>
<div class="absolute inset-0 pointer-events-none" aria-hidden="true">
<slot name="bg">
{bg ? <Fragment set:html={bg} /> : null}
</slot>
</div>
<section class="relative md:-mt-[76px] not-prose" {...id ? { id } : {}}>
{!disableBackground && (
<div class="absolute inset-0 pointer-events-none" aria-hidden="true">
<slot name="bg">
{bg ? <Fragment set:html={bg} /> : <Background isDark={isDark} disableParallax={true} />}
</slot>
</div>
)}
<div class="relative max-w-7xl mx-auto px-4 sm:px-6">
<div class="pt-0 md:pt-[76px] pointer-events-none"></div>
<div class="py-12 md:py-20">
@@ -39,7 +48,7 @@ const {
{
title && (
<h1
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade content-backdrop p-2 inline-block rounded-md"
set:html={title}
/>
)
@@ -48,14 +57,14 @@ const {
{
subtitle && (
<p
class="text-xl text-muted mb-6 dark:text-slate-300 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
class="text-xl text-muted mb-6 dark:text-slate-300 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade content-backdrop p-2 rounded-md"
set:html={subtitle}
/>
)
}
{
actions && (
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade">
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade content-backdrop p-2 rounded-md">
{Array.isArray(actions) ? (
actions.map((action) => (
<div class="flex w-full sm:w-auto">
@@ -76,7 +85,7 @@ const {
>
{
image && (
<div class="relative m-auto max-w-5xl">
<div class="relative m-auto max-w-5xl z-10">
{typeof image === 'string' ? (
<Fragment set:html={image} />
) : (

View File

@@ -1,6 +1,7 @@
---
import Image from '~/components/common/Image.astro';
import Button from '~/components/ui/Button.astro';
import Background from '~/components/ui/Background.astro';
import type { Hero as Props } from '~/types';
@@ -14,6 +15,7 @@ const {
image = await Astro.slots.render('image'),
id,
isDark = false,
bg = await Astro.slots.render('bg'),
} = Astro.props;
---
@@ -21,7 +23,7 @@ const {
<section class="relative md:-mt-[76px] not-prose" {...id ? { id } : {}}>
<div class="absolute inset-0 pointer-events-none" aria-hidden="true">
<slot name="bg">
{bg ? <Fragment set:html={bg} /> : null}
{bg ? <Fragment set:html={bg} /> : <Background isDark={isDark} showIcons={false} disableParallax={true} />}
</slot>
</div>
<div class="relative max-w-7xl mx-auto px-4 sm:px-6">
@@ -39,7 +41,7 @@ const {
{
title && (
<h1
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200 intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter"
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200 intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter content-backdrop p-2 inline-block rounded-md"
set:html={title}
/>
)
@@ -48,7 +50,7 @@ const {
{
subtitle && (
<p
class="text-xl text-muted mb-6 dark:text-slate-300 intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter"
class="text-xl text-muted mb-6 dark:text-slate-300 intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter content-backdrop p-2 rounded-md"
set:html={subtitle}
/>
)
@@ -56,7 +58,7 @@ const {
{
actions && (
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 lg:justify-start lg:m-0 lg:max-w-7xl intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter">
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 lg:justify-start lg:m-0 lg:max-w-7xl intersect-once motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter content-backdrop p-2 rounded-md">
{Array.isArray(actions) ? (
actions.map((action) => (
<div class="flex w-full sm:w-auto">
@@ -75,7 +77,7 @@ const {
<div class="basis-1/2">
{
image && (
<div class="relative m-auto max-w-5xl intersect-once intercept-no-queue motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter">
<div class="relative m-auto max-w-5xl z-10 intersect-once intercept-no-queue motion-safe:md:intersect:animate-fade motion-safe:md:opacity-0 intersect-quarter">
{typeof image === 'string' ? (
<Fragment set:html={image} />
) : (

View File

@@ -26,7 +26,7 @@ const {
{
testimonials &&
testimonials.map(({ title, linkUrl, name, issueDate, description, image }) => (
<a href={linkUrl} target="_blank" rel="noopener noreferrer" class="flex flex-col justify-start items-center p-2 md:p-4 rounded-md shadow-xl dark:shadow-none dark:border dark:border-slate-600 w-[425px] mx-auto sm:mx-0">
<a href={linkUrl} target="_blank" rel="noopener noreferrer" class="flex flex-col justify-start items-center p-2 md:p-4 rounded-md shadow-md backdrop-blur-sm bg-white/15 dark:bg-transparent border border-gray-200 dark:border-slate-600 hover:bg-white/30 hover:backdrop-blur-md dark:hover:backdrop-blur-md transition-all duration-300 ease-in-out hover:shadow-lg hover:translate-y-[-2px] w-[425px] mx-auto sm:mx-0">
{title && <h2 class="text-lg font-medium leading-6 pb-4 text-center">{title}</h2>}
{image && (
<div class="h-[160px] w-[160px] border-slate-200 dark:border-slate-600 mx-auto">
@@ -36,11 +36,8 @@ const {
height={160}
widths={[400, 768]}
layout="fixed"
// If image is a string, use it as the src; otherwise assume it's an object with a src property.
src={typeof image === 'string' ? image : image.src}
// Use a default alt text if image is a string; otherwise use the provided alt.
alt={typeof image === 'string' ? 'Image' : image.alt}
// Spread the rest of the properties if image is an object.
src={typeof image === 'string' ? image : (image as { src: string }).src}
alt={typeof image === 'string' ? 'Image' : (image as { alt?: string }).alt || 'Testimonial'}
{...(typeof image === 'string' ? {} : image)}
/>
</div>

View File

@@ -73,8 +73,8 @@ const timelineItems = items.map(item => {
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',
timeline: '',
timelineDot: 'bg-primary shadow-md dark:bg-blue-700',
year: 'text-primary dark:text-blue-300',
}}
/>

View File

@@ -4,8 +4,6 @@ site:
base: '/'
trailingSlash: ignore
googleSiteVerificationId: orcPxI47GSa-cRvY11tUe6iGg2IO_RPvnA1q95iEM3M
# Default SEO metadata
metadata:
title:

View File

@@ -11,6 +11,12 @@ export interface Translation {
skills: string;
education: string;
blog: string;
services: string;
contact: string;
};
footer: {
terms: string;
privacyPolicy: string;
};
hero: {
title: string;
@@ -76,6 +82,7 @@ export interface Translation {
items: {
title: string;
description: string;
icon?: string;
}[];
};
approach: {
@@ -137,11 +144,17 @@ export const translations: Record<string, Translation> = {
skills: 'Skills',
education: 'Education',
blog: 'Blog',
services: 'Services',
contact: 'Contact',
},
footer: {
terms: 'Terms',
privacyPolicy: 'Privacy Policy',
},
hero: {
title: 'Simplifying Systems, Amplifying Results',
greeting: 'Hi! I am Richard Bergsma.',
subtitle: 'I automate workflows with Power Automate, build smart chatbots in Copilot Studio, and connect systems through seamless API integrations. From optimizing IT infrastructure and managing global environments to enhancing collaboration with SharePoint and Azure, I streamline processes to make technology work smarter—all while leveling up my Python skills.',
title: 'Unlock Your Business Potential with Expert IT Automation',
greeting: 'Richard Bergsma, IT Systems & Automation Specialist',
subtitle: 'I deliver enterprise-grade automation solutions using Power Automate, Copilot Studio, and custom API development. My expertise helps businesses streamline operations, reduce costs, and improve productivity through Microsoft 365, SharePoint, and Azure.',
},
about: {
title: 'About me',
@@ -153,92 +166,98 @@ export const translations: Record<string, Translation> = {
},
homepage: {
actions: {
learnMore: 'Learn More',
contactMe: 'Contact Me',
learnMore: 'Explore Solutions',
contactMe: 'Request Consultation',
},
services: {
tagline: 'Services',
title: 'How I Can Help Your Organization',
subtitle: 'I offer a range of specialized IT services to help businesses optimize their operations and digital infrastructure.',
tagline: 'Automation & Integration Experts',
title: 'Drive Business Growth with Strategic IT Automation',
subtitle: 'Specialized IT solutions to optimize operations, enhance infrastructure, and deliver measurable results.',
items: [
{
title: 'Workflow Automation',
description: 'Streamline your business processes with Power Automate solutions that reduce manual effort and increase operational efficiency.',
description: 'Automate repetitive tasks and streamline processes with Power Automate, freeing up your team to focus on strategic initiatives and boosting overall efficiency.',
icon: 'tabler:settings-automation',
},
{
title: 'Intelligent Chatbots',
description: 'Develop smart chatbots in Copilot Studio that enhance user interactions through natural language processing and automated responses.',
description: 'Enhance customer service and employee productivity with AI-powered chatbots built in Copilot Studio, providing instant support and personalized experiences.',
icon: 'tabler:message-chatbot',
},
{
title: 'API Integrations',
description: 'Create seamless connections between your applications and services with custom API integrations for efficient data exchange.',
description: 'Connect your critical applications and services with seamless API integrations, enabling efficient data exchange and automated workflows across your entire ecosystem.',
icon: 'tabler:api',
},
{
title: 'Microsoft 365 Management',
description: 'Optimize your Microsoft 365 environment with expert administration, security configurations, and service optimization.',
description: 'Maximize the value of your Microsoft 365 investment with expert administration, proactive security, and ongoing optimization to ensure a secure and productive environment.',
icon: 'tabler:brand-office',
},
{
title: 'SharePoint Solutions',
description: 'Set up, manage, and optimize SharePoint Online and on-premise deployments for effective document management and collaboration.',
description: 'Transform your document management and collaboration with tailored SharePoint solutions that streamline workflows, improve information sharing, and enhance team productivity.',
icon: 'tabler:share',
},
{
title: 'IT Infrastructure Oversight',
description: 'Manage global IT infrastructures, including servers, networks, and end-user devices to ensure reliable operations.',
description: 'Ensure reliable and efficient IT operations with proactive infrastructure management, minimizing downtime and maximizing performance across your global environment.',
icon: 'tabler:server',
},
],
},
approach: {
tagline: 'About My Approach',
title: 'Driving IT Excellence Through Innovation',
missionTitle: 'Mission Statement',
tagline: 'Strategic & Results-Driven Approach',
title: 'Transforming Your Business with Proven IT Strategies',
missionTitle: 'Our Commitment',
missionContent: [
'My mission is to drive IT excellence by optimizing cloud solutions, automating processes, and providing outstanding technical support. I believe in leveraging technology to solve real business challenges and create value through innovation.',
'With over 15 years of IT experience, I bring a wealth of knowledge in Microsoft technologies, automation tools, and system integration to help organizations transform their digital capabilities and achieve their strategic goals.'
'We are committed to driving IT excellence through strategic cloud optimization, process automation, and enterprise-grade technical support. We leverage cutting-edge technology to address complex business challenges and deliver measurable value.',
'With deep expertise in Microsoft technologies and automation, we empower organizations to transform their digital capabilities and achieve their business objectives.'
],
items: [
{
title: 'User-Centric Solutions',
description: 'I focus on creating solutions that enhance user experience and productivity, ensuring technology serves people effectively.',
description: 'We design solutions that enhance user experience and maximize productivity, ensuring technology empowers your business.',
},
{
title: 'Continuous Improvement',
description: 'I stay current with emerging technologies and best practices to deliver cutting-edge solutions that evolve with your needs.',
title: 'Continuous Innovation',
description: 'We stay ahead of the curve by researching and implementing emerging technologies, providing scalable solutions that adapt to your evolving needs.',
},
{
title: 'Strategic Implementation',
description: 'I approach each project strategically, aligning technical solutions with business objectives for maximum impact.',
description: 'We align technical solutions with your core business objectives, delivering measurable ROI and a competitive advantage.',
},
],
},
testimonials: {
tagline: 'Testimonials',
title: 'What Clients Say About My Work',
tagline: 'Real Results for Our Clients',
title: 'What Our Clients Are Saying',
items: [
{
testimonial: 'Richard\'s expertise in Power Automate transformed our workflow processes, saving us countless hours and reducing errors significantly.',
name: 'Client Name',
description: 'Position, Company',
testimonial: 'Richard\'s Power Automate expertise was instrumental in automating our invoice processing, reducing manual effort by 70% and eliminating data entry errors. The ROI was immediate and significant.',
name: 'John Smith',
description: 'CFO, Acme Corp',
},
{
testimonial: 'The SharePoint implementation Richard delivered has revolutionized our document management and team collaboration capabilities.',
name: 'Client Name',
description: 'Position, Company',
testimonial: 'The SharePoint implementation Richard delivered completely transformed our team\'s ability to collaborate and share information. We\'ve seen a dramatic increase in productivity and a significant reduction in email clutter.',
name: 'Jane Doe',
description: 'Project Manager, Beta Industries',
},
{
testimonial: 'Richard\'s technical knowledge combined with his ability to understand our business needs resulted in solutions that truly addressed our challenges.',
name: 'Client Name',
description: 'Position, Company',
testimonial: 'Richard took the time to truly understand our unique business challenges and developed customized IT solutions that perfectly addressed our needs. His technical knowledge and problem-solving skills are exceptional.',
name: 'David Lee',
description: 'CEO, Gamma Solutions',
},
],
},
callToAction: {
title: 'Ready to optimize your IT systems?',
subtitle: 'Let\'s discuss how I can help your organization streamline processes, enhance collaboration, and drive digital transformation.',
button: 'Contact Me',
title: 'Take Control of Your IT Future',
subtitle: 'Let\'s discuss how our solutions can streamline your processes, improve collaboration, and drive digital transformation.',
button: 'Schedule Your Consultation Now',
},
contact: {
title: 'Get in Touch',
subtitle: 'Have a project in mind or questions about my services? Reach out and let\'s start a conversation.',
title: 'Contact Our Team',
subtitle: 'Discuss your enterprise requirements or inquire about our professional services. Our consultants are ready to provide expert guidance tailored to your business needs.',
nameLabel: 'Name',
namePlaceholder: 'Your name',
emailLabel: 'Email',
@@ -246,7 +265,7 @@ export const translations: Record<string, Translation> = {
messageLabel: 'Message',
messagePlaceholder: 'Your message',
disclaimer: 'By submitting this form, you agree to our privacy policy and allow us to use your information to contact you about our services.',
description: 'I\'ll respond to your message as soon as possible. You can also connect with me on LinkedIn or GitHub.',
description: 'All inquiries receive a prompt professional response. For additional information about our enterprise solutions, connect with our team on LinkedIn or explore our technical resources on GitHub.',
},
},
resume: {
@@ -476,11 +495,17 @@ export const translations: Record<string, Translation> = {
skills: 'Vaardigheden',
education: 'Opleiding',
blog: 'Blog',
services: 'Diensten',
contact: 'Contact',
},
footer: {
terms: 'Voorwaarden',
privacyPolicy: 'Privacybeleid',
},
hero: {
title: 'Systemen vereenvoudigen, Resultaten versterken',
greeting: 'Hoi! Ik ben Richard Bergsma.',
subtitle: 'Ik automatiseer werkstromen met Power Automate, bouw slimme chatbots in Copilot Studio en verbind systemen via naadloze API-integraties. Van het optimaliseren van IT-infrastructuur en het beheren van wereldwijde omgevingen tot het verbeteren van samenwerking met SharePoint en Azure, stroomlijn ik processen om technologie slimmer te laten werken terwijl ik mijn Python-vaardigheden verder ontwikkel.',
title: 'Ontgrendel uw zakelijk potentieel met expert IT-automatisering',
greeting: 'Richard Bergsma, IT-systemen & Automatisering Specialist',
subtitle: 'Ik lever automatiseringsoplossingen van enterprise-niveau met Power Automate, Copilot Studio en aangepaste API-ontwikkeling. Mijn expertise helpt bedrijven om processen te stroomlijnen, kosten te verlagen en productiviteit te verbeteren via Microsoft 365, SharePoint en Azure.',
},
about: {
title: 'Over mij',
@@ -492,92 +517,98 @@ export const translations: Record<string, Translation> = {
},
homepage: {
actions: {
learnMore: 'Meer informatie',
contactMe: 'Neem contact op',
learnMore: 'Verken oplossingen',
contactMe: 'Vraag een consultatie aan',
},
services: {
tagline: 'Diensten',
title: 'Hoe ik uw organisatie kan helpen',
subtitle: 'Ik bied een reeks gespecialiseerde IT-diensten om bedrijven te helpen hun activiteiten en digitale infrastructuur te optimaliseren.',
tagline: 'Automatisering & Integratie Experts',
title: 'Stimuleer bedrijfsgroei met strategische IT-automatisering',
subtitle: 'Gespecialiseerde IT-oplossingen om operaties te optimaliseren, infrastructuur te verbeteren en meetbare resultaten te leveren.',
items: [
{
title: 'Workflow Automatisering',
description: 'Stroomlijn uw bedrijfsprocessen met Power Automate-oplossingen die handmatige inspanning verminderen en de operationele efficiëntie verhogen.',
description: 'Automatiseer repetitieve taken en stroomlijn processen met Power Automate, waardoor uw team zich kan richten op strategische initiatieven en de algehele efficiëntie wordt verhoogd.',
icon: 'tabler:settings-automation',
},
{
title: 'Intelligente Chatbots',
description: 'Ontwikkel slimme chatbots in Copilot Studio die gebruikersinteracties verbeteren door natuurlijke taalverwerking en geautomatiseerde antwoorden.',
description: 'Verbeter klantenservice en werknemersproductiviteit met AI-gestuurde chatbots gebouwd in Copilot Studio, die directe ondersteuning en gepersonaliseerde ervaringen bieden.',
icon: 'tabler:message-chatbot',
},
{
title: 'API-integraties',
description: 'Creëer naadloze verbindingen tussen uw applicaties en diensten met aangepaste API-integraties voor efficiënte gegevensuitwisseling.',
description: 'Verbind uw kritieke applicaties en diensten met naadloze API-integraties, waardoor efficiënte gegevensuitwisseling en geautomatiseerde workflows in uw hele ecosysteem mogelijk worden.',
icon: 'tabler:api',
},
{
title: 'Microsoft 365 Beheer',
description: 'Optimaliseer uw Microsoft 365-omgeving met deskundig beheer, beveiligingsconfiguraties en service-optimalisatie.',
description: 'Maximaliseer de waarde van uw Microsoft 365-investering met deskundig beheer, proactieve beveiliging en voortdurende optimalisatie voor een veilige en productieve omgeving.',
icon: 'tabler:brand-office',
},
{
title: 'SharePoint Oplossingen',
description: 'Opzetten, beheren en optimaliseren van SharePoint Online en on-premise implementaties voor effectief documentbeheer en samenwerking.',
description: 'Transformeer uw documentbeheer en samenwerking met op maat gemaakte SharePoint-oplossingen die workflows stroomlijnen, informatie-uitwisseling verbeteren en teamproductiviteit verhogen.',
icon: 'tabler:share',
},
{
title: 'IT-infrastructuur Toezicht',
description: 'Beheer van wereldwijde IT-infrastructuren, inclusief servers, netwerken en eindgebruikersapparaten om betrouwbare operaties te garanderen.',
description: 'Zorg voor betrouwbare en efficiënte IT-operaties met proactief infrastructuurbeheer, minimaliseer downtime en maximaliseer prestaties in uw wereldwijde omgeving.',
icon: 'tabler:server',
},
],
},
approach: {
tagline: 'Over Mijn Aanpak',
title: 'IT-excellentie stimuleren door innovatie',
missionTitle: 'Missie',
tagline: 'Strategische & Resultaatgerichte Aanpak',
title: 'Uw bedrijf transformeren met bewezen IT-strategieën',
missionTitle: 'Onze Toewijding',
missionContent: [
'Mijn missie is om IT-excellentie te stimuleren door cloudoplossingen te optimaliseren, processen te automatiseren en uitstekende technische ondersteuning te bieden. Ik geloof in het benutten van technologie om echte zakelijke uitdagingen op te lossen en waarde te creëren door innovatie.',
'Met meer dan 15 jaar IT-ervaring breng ik een schat aan kennis in Microsoft-technologieën, automatiseringstools en systeemintegratie om organisaties te helpen hun digitale mogelijkheden te transformeren en hun strategische doelen te bereiken.'
'Wij zijn toegewijd aan het stimuleren van IT-excellentie door strategische cloud-optimalisatie, procesautomatisering en technische ondersteuning van enterprise-niveau. We benutten geavanceerde technologie om complexe zakelijke uitdagingen aan te pakken en meetbare waarde te leveren.',
'Met diepgaande expertise in Microsoft-technologieën en automatisering stellen we organisaties in staat hun digitale mogelijkheden te transformeren en hun bedrijfsdoelstellingen te bereiken.'
],
items: [
{
title: 'Gebruikersgerichte Oplossingen',
description: 'Ik focus op het creëren van oplossingen die de gebruikerservaring en productiviteit verbeteren, zodat technologie mensen effectief dient.',
description: 'We ontwerpen oplossingen die de gebruikerservaring verbeteren en productiviteit maximaliseren, zodat technologie uw bedrijf versterkt.',
},
{
title: 'Continue Verbetering',
description: 'Ik blijf op de hoogte van opkomende technologieën en best practices om geavanceerde oplossingen te leveren die meegroeien met uw behoeften.',
title: 'Continue Innovatie',
description: 'We blijven voorop lopen door onderzoek en implementatie van opkomende technologieën, en bieden schaalbare oplossingen die zich aanpassen aan uw evoluerende behoeften.',
},
{
title: 'Strategische Implementatie',
description: 'Ik benader elk project strategisch, waarbij ik technische oplossingen afstem op bedrijfsdoelstellingen voor maximale impact.',
description: 'We stemmen technische oplossingen af op uw kernbedrijfsdoelstellingen, wat resulteert in meetbare ROI en een concurrentievoordeel.',
},
],
},
testimonials: {
tagline: 'Getuigenissen',
title: 'Wat klanten zeggen over mijn werk',
tagline: 'Echte Resultaten voor Onze Klanten',
title: 'Wat Onze Klanten Zeggen',
items: [
{
testimonial: 'De expertise van Richard in Power Automate heeft onze werkstroomprocessen getransformeerd, waardoor we talloze uren hebben bespaard en fouten aanzienlijk zijn verminderd.',
name: 'Klantnaam',
description: 'Functie, Bedrijf',
testimonial: 'Richards expertise in Power Automate was essentieel bij het automatiseren van onze factuurverwerking, waardoor handmatige inspanning met 70% werd verminderd en invoerfouten werden geëlimineerd. De ROI was direct en significant.',
name: 'John Smith',
description: 'CFO, Acme Corp',
},
{
testimonial: 'De SharePoint-implementatie die Richard heeft geleverd, heeft onze documentbeheer- en teamsamenwerkingsmogelijkheden revolutionair veranderd.',
name: 'Klantnaam',
description: 'Functie, Bedrijf',
testimonial: 'De SharePoint-implementatie die Richard heeft geleverd, heeft de mogelijkheden van ons team om samen te werken en informatie te delen volledig getransformeerd. We hebben een dramatische toename in productiviteit gezien en een aanzienlijke vermindering van e-mailverkeer.',
name: 'Jane Doe',
description: 'Projectmanager, Beta Industries',
},
{
testimonial: 'De technische kennis van Richard in combinatie met zijn vermogen om onze zakelijke behoeften te begrijpen, resulteerde in oplossingen die echt onze uitdagingen aanpakten.',
name: 'Klantnaam',
description: 'Functie, Bedrijf',
testimonial: 'Richard nam de tijd om onze unieke zakelijke uitdagingen echt te begrijpen en ontwikkelde aangepaste IT-oplossingen die perfect aan onze behoeften voldeden. Zijn technische kennis en probleemoplossende vaardigheden zijn uitzonderlijk.',
name: 'David Lee',
description: 'CEO, Gamma Solutions',
},
],
},
callToAction: {
title: 'Klaar om uw IT-systemen te optimaliseren?',
subtitle: 'Laten we bespreken hoe ik uw organisatie kan helpen processen te stroomlijnen, samenwerking te verbeteren en digitale transformatie te stimuleren.',
button: 'Neem contact op',
title: 'Neem de controle over uw IT-toekomst',
subtitle: 'Laten we bespreken hoe onze oplossingen uw processen kunnen stroomlijnen, samenwerking kunnen verbeteren en digitale transformatie kunnen stimuleren.',
button: 'Plan nu uw consultatie',
},
contact: {
title: 'Neem contact op',
subtitle: 'Heeft u een project in gedachten of vragen over mijn diensten? Neem contact op en laten we een gesprek beginnen.',
title: 'Neem contact op met ons team',
subtitle: 'Bespreek uw zakelijke vereisten of informeer naar onze professionele diensten. Onze consultants staan klaar om deskundige begeleiding te bieden die is afgestemd op uw bedrijfsbehoeften.',
nameLabel: 'Naam',
namePlaceholder: 'Uw naam',
emailLabel: 'E-mail',
@@ -585,7 +616,7 @@ export const translations: Record<string, Translation> = {
messageLabel: 'Bericht',
messagePlaceholder: 'Uw bericht',
disclaimer: 'Door dit formulier in te dienen, gaat u akkoord met ons privacybeleid en staat u ons toe uw gegevens te gebruiken om contact met u op te nemen over onze diensten.',
description: 'Ik zal zo snel mogelijk op uw bericht reageren. U kunt ook verbinding maken met mij op LinkedIn of GitHub.',
description: 'Alle vragen ontvangen een snelle professionele reactie. Voor aanvullende informatie over onze zakelijke oplossingen kunt u contact opnemen met ons team op LinkedIn of onze technische bronnen op GitHub verkennen.',
},
},
resume: {
@@ -815,11 +846,17 @@ export const translations: Record<string, Translation> = {
skills: 'Fähigkeiten',
education: 'Ausbildung',
blog: 'Blog',
services: 'Dienstleistungen',
contact: 'Kontakt',
},
footer: {
terms: 'Nutzungsbedingungen',
privacyPolicy: 'Datenschutzrichtlinie',
},
hero: {
title: 'Systeme vereinfachen, Ergebnisse verstärken',
greeting: 'Hallo! Ich bin Richard Bergsma.',
subtitle: 'Ich automatisiere Arbeitsabläufe mit Power Automate, entwickle intelligente Chatbots in Copilot Studio und verbinde Systeme durch nahtlose API-Integrationen. Von der Optimierung der IT-Infrastruktur und dem Management globaler Umgebungen bis hin zur Verbesserung der Zusammenarbeit mit SharePoint und Azure, optimiere ich Prozesse, um Technologie intelligenter arbeiten zu lassen während ich meine Python-Fähigkeiten weiterentwickle.',
title: 'Erschließen Sie Ihr Geschäftspotenzial mit Expert-IT-Automatisierung',
greeting: 'Richard Bergsma, IT-Systeme & Automatisierungsspezialist',
subtitle: 'Ich liefere Automatisierungslösungen auf Unternehmensniveau mit Power Automate, Copilot Studio und maßgeschneiderter API-Entwicklung. Meine Expertise hilft Unternehmen, Prozesse zu optimieren, Kosten zu senken und die Produktivität durch Microsoft 365, SharePoint und Azure zu steigern.',
},
about: {
title: 'Über mich',
@@ -831,92 +868,98 @@ export const translations: Record<string, Translation> = {
},
homepage: {
actions: {
learnMore: 'Mehr erfahren',
contactMe: 'Kontaktieren Sie mich',
learnMore: 'Lösungen erkunden',
contactMe: 'Beratung anfragen',
},
services: {
tagline: 'Dienstleistungen',
title: 'Wie ich Ihrer Organisation helfen kann',
subtitle: 'Ich biete eine Reihe spezialisierter IT-Dienstleistungen an, um Unternehmen bei der Optimierung ihrer Abläufe und digitalen Infrastruktur zu unterstützen.',
tagline: 'Automatisierungs- & Integrations-Experten',
title: 'Fördern Sie Unternehmenswachstum mit strategischer IT-Automatisierung',
subtitle: 'Spezialisierte IT-Lösungen zur Optimierung von Abläufen, Verbesserung der Infrastruktur und Lieferung messbarer Ergebnisse.',
items: [
{
title: 'Workflow-Automatisierung',
description: 'Optimieren Sie Ihre Geschäftsprozesse mit Power Automate-Lösungen, die den manuellen Aufwand reduzieren und die betriebliche Effizienz steigern.',
description: 'Automatisieren Sie wiederkehrende Aufgaben und optimieren Sie Prozesse mit Power Automate, damit sich Ihr Team auf strategische Initiativen konzentrieren kann und die Gesamteffizienz gesteigert wird.',
icon: 'tabler:settings-automation',
},
{
title: 'Intelligente Chatbots',
description: 'Entwickeln Sie smarte Chatbots in Copilot Studio, die Benutzerinteraktionen durch natürliche Sprachverarbeitung und automatisierte Antworten verbessern.',
description: 'Verbessern Sie Kundenservice und Mitarbeiterproduktivität mit KI-gestützten Chatbots, die in Copilot Studio erstellt wurden und sofortige Unterstützung sowie personalisierte Erfahrungen bieten.',
icon: 'tabler:message-chatbot',
},
{
title: 'API-Integrationen',
description: 'Schaffen Sie nahtlose Verbindungen zwischen Ihren Anwendungen und Diensten mit benutzerdefinierten API-Integrationen für effizienten Datenaustausch.',
description: 'Verbinden Sie Ihre kritischen Anwendungen und Dienste mit nahtlosen API-Integrationen, die effizienten Datenaustausch und automatisierte Workflows in Ihrem gesamten Ökosystem ermöglichen.',
icon: 'tabler:api',
},
{
title: 'Microsoft 365 Management',
description: 'Optimieren Sie Ihre Microsoft 365-Umgebung mit fachkundiger Administration, Sicherheitskonfigurationen und Service-Optimierung.',
description: 'Maximieren Sie den Wert Ihrer Microsoft 365-Investition mit fachkundiger Administration, proaktiver Sicherheit und kontinuierlicher Optimierung für eine sichere und produktive Umgebung.',
icon: 'tabler:brand-office',
},
{
title: 'SharePoint-Lösungen',
description: 'Einrichten, Verwalten und Optimieren von SharePoint Online und On-Premise-Implementierungen für effektives Dokumentenmanagement und Zusammenarbeit.',
description: 'Transformieren Sie Ihr Dokumentenmanagement und Ihre Zusammenarbeit mit maßgeschneiderten SharePoint-Lösungen, die Workflows optimieren, Informationsaustausch verbessern und Teamproduktivität steigern.',
icon: 'tabler:share',
},
{
title: 'IT-Infrastrukturüberwachung',
description: 'Verwaltung globaler IT-Infrastrukturen, einschließlich Server, Netzwerke und Endbenutzergeräte, um zuverlässige Betriebsabläufe zu gewährleisten.',
description: 'Sorgen Sie für zuverlässige und effiziente IT-Abläufe mit proaktivem Infrastrukturmanagement, minimieren Sie Ausfallzeiten und maximieren Sie die Leistung in Ihrer globalen Umgebung.',
icon: 'tabler:server',
},
],
},
approach: {
tagline: 'Über meinen Ansatz',
title: 'IT-Exzellenz durch Innovation vorantreiben',
missionTitle: 'Leitbild',
tagline: 'Strategischer & Ergebnisorientierter Ansatz',
title: 'Transformation Ihres Unternehmens mit bewährten IT-Strategien',
missionTitle: 'Unser Engagement',
missionContent: [
'Meine Mission ist es, IT-Exzellenz durch die Optimierung von Cloud-Lösungen, die Automatisierung von Prozessen und die Bereitstellung hervorragender technischer Unterstützung voranzutreiben. Ich glaube daran, Technologie zu nutzen, um echte geschäftliche Herausforderungen zu lösen und durch Innovation Mehrwert zu schaffen.',
'Mit über 15 Jahren IT-Erfahrung bringe ich einen reichen Schatz an Wissen in Microsoft-Technologien, Automatisierungstools und Systemintegration mit, um Organisationen dabei zu helfen, ihre digitalen Fähigkeiten zu transformieren und ihre strategischen Ziele zu erreichen.'
'Wir sind bestrebt, IT-Exzellenz durch strategische Cloud-Optimierung, Prozessautomatisierung und technischen Support auf Unternehmensebene voranzutreiben. Wir nutzen modernste Technologie, um komplexe geschäftliche Herausforderungen zu bewältigen und messbaren Mehrwert zu liefern.',
'Mit tiefgreifender Expertise in Microsoft-Technologien und Automatisierung befähigen wir Organisationen, ihre digitalen Fähigkeiten zu transformieren und ihre Geschäftsziele zu erreichen.'
],
items: [
{
title: 'Nutzerzentrierte Lösungen',
description: 'Ich konzentriere mich auf die Entwicklung von Lösungen, die die Benutzererfahrung und Produktivität verbessern und sicherstellen, dass Technologie den Menschen effektiv dient.',
description: 'Wir entwickeln Lösungen, die die Benutzererfahrung verbessern und die Produktivität maximieren, damit Technologie Ihr Unternehmen stärkt.',
},
{
title: 'Kontinuierliche Verbesserung',
description: 'Ich bleibe auf dem Laufenden mit aufkommenden Technologien und Best Practices, um innovative Lösungen zu liefern, die mit Ihren Bedürfnissen wachsen.',
title: 'Kontinuierliche Innovation',
description: 'Wir bleiben an der Spitze durch Erforschung und Implementierung aufkommender Technologien und bieten skalierbare Lösungen, die sich an Ihre sich entwickelnden Anforderungen anpassen.',
},
{
title: 'Strategische Implementierung',
description: 'Ich gehe strategisch an jedes Projekt heran und stimme technische Lösungen mit Geschäftszielen ab, um maximale Wirkung zu erzielen.',
title: 'Strategische Umsetzung',
description: 'Wir stimmen technische Lösungen auf Ihre Kerngeschäftsziele ab und liefern messbaren ROI und Wettbewerbsvorteile.',
},
],
},
testimonials: {
tagline: 'Referenzen',
title: 'Was Kunden über meine Arbeit sagen',
tagline: 'Echte Ergebnisse für unsere Kunden',
title: 'Was unsere Kunden sagen',
items: [
{
testimonial: 'Richards Expertise in Power Automate hat unsere Workflow-Prozesse transformiert, uns unzählige Stunden gespart und Fehler erheblich reduziert.',
name: 'Kundenname',
description: 'Position, Unternehmen',
testimonial: 'Richards Expertise in Power Automate war entscheidend bei der Automatisierung unserer Rechnungsverarbeitung, wodurch der manuelle Aufwand um 70% reduziert und Dateneingabefehler eliminiert wurden. Der ROI war sofort und signifikant.',
name: 'John Smith',
description: 'CFO, Acme Corp',
},
{
testimonial: 'Die von Richard gelieferte SharePoint-Implementierung hat unsere Dokumentenmanagement- und Teamkollaborationsfähigkeiten revolutioniert.',
name: 'Kundenname',
description: 'Position, Unternehmen',
testimonial: 'Die von Richard gelieferte SharePoint-Implementierung hat die Fähigkeit unseres Teams zur Zusammenarbeit und zum Informationsaustausch komplett transformiert. Wir haben einen dramatischen Anstieg der Produktivität und eine erhebliche Reduzierung von E-Mail-Überflutung erlebt.',
name: 'Jane Doe',
description: 'Projektmanager, Beta Industries',
},
{
testimonial: 'Richards technisches Wissen in Kombination mit seiner Fähigkeit, unsere Geschäftsanforderungen zu verstehen, führte zu Lösungen, die unsere Herausforderungen wirklich adressierten.',
name: 'Kundenname',
description: 'Position, Unternehmen',
testimonial: 'Richard nahm sich die Zeit, unsere einzigartigen geschäftlichen Herausforderungen wirklich zu verstehen und entwickelte maßgeschneiderte IT-Lösungen, die unsere Bedürfnisse perfekt adressierten. Seine technischen Kenntnisse und Problemlösungsfähigkeiten sind außergewöhnlich.',
name: 'David Lee',
description: 'CEO, Gamma Solutions',
},
],
},
callToAction: {
title: 'Bereit, Ihre IT-Systeme zu optimieren?',
subtitle: 'Lassen Sie uns besprechen, wie ich Ihrer Organisation helfen kann, Prozesse zu optimieren, die Zusammenarbeit zu verbessern und die digitale Transformation voranzutreiben.',
button: 'Kontaktieren Sie mich',
title: 'Übernehmen Sie die Kontrolle über Ihre IT-Zukunft',
subtitle: 'Lassen Sie uns besprechen, wie unsere Lösungen Ihre Prozesse optimieren, die Zusammenarbeit verbessern und die digitale Transformation vorantreiben können.',
button: 'Planen Sie jetzt Ihre Beratung',
},
contact: {
title: 'Kontakt aufnehmen',
subtitle: 'Haben Sie ein Projekt im Sinn oder Fragen zu meinen Dienstleistungen? Nehmen Sie Kontakt auf und lassen Sie uns ein Gespräch beginnen.',
title: 'Kontaktieren Sie unser Team',
subtitle: 'Besprechen Sie Ihre Unternehmensanforderungen oder erkundigen Sie sich nach unseren professionellen Dienstleistungen. Unsere Berater stehen bereit, um fachkundige Beratung zu bieten, die auf Ihre geschäftlichen Bedürfnisse zugeschnitten ist.',
nameLabel: 'Name',
namePlaceholder: 'Ihr Name',
emailLabel: 'E-Mail',
@@ -924,7 +967,7 @@ export const translations: Record<string, Translation> = {
messageLabel: 'Nachricht',
messagePlaceholder: 'Ihre Nachricht',
disclaimer: 'Durch das Absenden dieses Formulars stimmen Sie unserer Datenschutzrichtlinie zu und erlauben uns, Ihre Informationen zu verwenden, um Sie über unsere Dienstleistungen zu kontaktieren.',
description: 'Ich werde so schnell wie möglich auf Ihre Nachricht antworten. Sie können sich auch über LinkedIn oder GitHub mit mir verbinden.',
description: 'Alle Anfragen erhalten eine schnelle professionelle Antwort. Für weitere Informationen über unsere Unternehmenslösungen können Sie sich mit unserem Team auf LinkedIn verbinden oder unsere technischen Ressourcen auf GitHub erkunden.',
},
},
resume: {
@@ -1154,11 +1197,17 @@ export const translations: Record<string, Translation> = {
skills: 'Compétences',
education: 'Formation',
blog: 'Blog',
services: 'Services',
contact: 'Contact',
},
footer: {
terms: 'Conditions d\'utilisation',
privacyPolicy: 'Politique de confidentialité',
},
hero: {
title: 'Simplifier les Systèmes, Amplifier les Résultats',
greeting: 'Bonjour ! Je suis Richard Bergsma.',
subtitle: 'J\'automatise les flux de travail avec Power Automate, développe des chatbots intelligents dans Copilot Studio et connecte les systèmes via des intégrations API transparentes. De l\'optimisation de l\'infrastructure IT et la gestion des environnements globaux à l\'amélioration de la collaboration avec SharePoint et Azure, je rationalise les processus pour rendre la technologie plus intelligente, tout en perfectionnant mes compétences en Python.',
title: 'Libérez votre potentiel d\'entreprise avec l\'automatisation IT experte',
greeting: 'Richard Bergsma, spécialiste des systèmes IT et de l\'automatisation',
subtitle: 'Je propose des solutions d\'automatisation de niveau entreprise utilisant Power Automate, Copilot Studio et le développement d\'API personnalisées. Mon expertise aide les entreprises à rationaliser leurs opérations, réduire les coûts et améliorer la productivité grâce à Microsoft 365, SharePoint et Azure.',
},
about: {
title: 'À propos de moi',
@@ -1170,92 +1219,98 @@ export const translations: Record<string, Translation> = {
},
homepage: {
actions: {
learnMore: 'En savoir plus',
contactMe: 'Me contacter',
learnMore: 'Explorer les solutions',
contactMe: 'Demander une consultation',
},
services: {
tagline: 'Services',
title: 'Comment je peux aider votre organisation',
subtitle: 'Je propose une gamme de services IT spécialisés pour aider les entreprises à optimiser leurs opérations et leur infrastructure numérique.',
tagline: 'Experts en automatisation et intégration',
title: 'Stimulez la croissance de votre entreprise grâce à l\'automatisation IT stratégique',
subtitle: 'Solutions IT spécialisées pour optimiser les opérations, améliorer l\'infrastructure et fournir des résultats mesurables.',
items: [
{
title: 'Automatisation des flux de travail',
description: 'Rationalisez vos processus métier avec des solutions Power Automate qui réduisent l\'effort manuel et augmentent l\'efficacité opérationnelle.',
description: 'Automatisez les tâches répétitives et rationalisez les processus avec Power Automate, libérant votre équipe pour se concentrer sur les initiatives stratégiques et augmentant l\'efficacité globale.',
icon: 'tabler:settings-automation',
},
{
title: 'Chatbots intelligents',
description: 'Développez des chatbots intelligents dans Copilot Studio qui améliorent les interactions utilisateur grâce au traitement du langage naturel et aux réponses automatisées.',
description: 'Améliorez le service client et la productivité des employés avec des chatbots alimentés par l\'IA construits dans Copilot Studio, offrant un support instantané et des expériences personnalisées.',
icon: 'tabler:message-chatbot',
},
{
title: 'Intégrations API',
description: 'Créez des connexions transparentes entre vos applications et services avec des intégrations API personnalisées pour un échange de données efficace.',
description: 'Connectez vos applications et services critiques avec des intégrations API transparentes, permettant un échange de données efficace et des flux de travail automatisés dans tout votre écosystème.',
icon: 'tabler:api',
},
{
title: 'Gestion Microsoft 365',
description: 'Optimisez votre environnement Microsoft 365 avec une administration experte, des configurations de sécurité et une optimisation des services.',
description: 'Maximisez la valeur de votre investissement Microsoft 365 avec une administration experte, une sécurité proactive et une optimisation continue pour assurer un environnement sécurisé et productif.',
icon: 'tabler:brand-office',
},
{
title: 'Solutions SharePoint',
description: 'Configurez, gérez et optimisez les déploiements SharePoint Online et on-premise pour une gestion efficace des documents et une collaboration optimale.',
description: 'Transformez votre gestion documentaire et votre collaboration avec des solutions SharePoint sur mesure qui rationalisent les flux de travail, améliorent le partage d\'informations et renforcent la productivité des équipes.',
icon: 'tabler:share',
},
{
title: 'Supervision de l\'infrastructure IT',
description: 'rez les infrastructures IT mondiales, y compris les serveurs, les réseaux et les appareils des utilisateurs finaux pour assurer des opérations fiables.',
description: 'Assurez des opérations IT fiables et efficaces avec une gestion proactive de l\'infrastructure, minimisant les temps d\'arrêt et maximisant les performances dans votre environnement mondial.',
icon: 'tabler:server',
},
],
},
approach: {
tagline: 'À propos de mon approche',
title: 'Favoriser l\'excellence IT par l\'innovation',
missionTitle: 'Énoncé de mission',
tagline: 'Approche stratégique et axée sur les résultats',
title: 'Transformer votre entreprise avec des stratégies IT éprouvées',
missionTitle: 'Notre engagement',
missionContent: [
'Ma mission est de favoriser l\'excellence IT en optimisant les solutions cloud, en automatisant les processus et en fournissant un support technique exceptionnel. Je crois en l\'utilisation de la technologie pour résoudre de véritables défis commerciaux et créer de la valeur grâce à l\'innovation.',
'Avec plus de 15 ans d\'expérience en IT, j\'apporte une richesse de connaissances en technologies Microsoft, outils d\'automatisation et intégration de systèmes pour aider les organisations à transformer leurs capacités numériques et à atteindre leurs objectifs stratégiques.'
'Nous nous engageons à favoriser l\'excellence IT grâce à l\'optimisation stratégique du cloud, l\'automatisation des processus et le support technique de niveau entreprise. Nous exploitons les technologies de pointe pour relever les défis commerciaux complexes et fournir une valeur mesurable.',
'Avec une expertise approfondie dans les technologies Microsoft et l\'automatisation, nous permettons aux organisations de transformer leurs capacités numériques et d\'atteindre leurs objectifs commerciaux.'
],
items: [
{
title: 'Solutions centrées sur l\'utilisateur',
description: 'Je me concentre sur la création de solutions qui améliorent l\'expérience utilisateur et la productivité, en veillant à ce que la technologie serve efficacement les personnes.',
description: 'Nous concevons des solutions qui améliorent l\'expérience utilisateur et maximisent la productivité, garantissant que la technologie renforce votre entreprise.',
},
{
title: 'Amélioration continue',
description: 'Je reste à jour avec les technologies émergentes et les meilleures pratiques pour fournir des solutions de pointe qui évoluent avec vos besoins.',
title: 'Innovation continue',
description: 'Nous restons à la pointe en recherchant et en implémentant des technologies émergentes, fournissant des solutions évolutives qui s\'adaptent à vos besoins en constante évolution.',
},
{
title: 'Implémentation stratégique',
description: 'J\'aborde chaque projet de manière stratégique, en alignant les solutions techniques sur les objectifs commerciaux pour un impact maximal.',
title: 'Mise en œuvre stratégique',
description: 'Nous alignons les solutions techniques sur vos objectifs commerciaux fondamentaux, offrant un ROI mesurable et un avantage concurrentiel.',
},
],
},
testimonials: {
tagline: 'Témoignages',
title: 'Ce que les clients disent de mon travail',
tagline: 'Résultats concrets pour nos clients',
title: 'Ce que disent nos clients',
items: [
{
testimonial: 'L\'expertise de Richard en Power Automate a transformé nos processus de flux de travail, nous faisant gagner d\'innombrables heures et réduisant considérablement les erreurs.',
name: 'Nom du client',
description: 'Poste, Entreprise',
testimonial: 'L\'expertise de Richard en Power Automate a été déterminante dans l\'automatisation de notre traitement des factures, réduisant l\'effort manuel de 70% et éliminant les erreurs de saisie. Le ROI a été immédiat et significatif.',
name: 'John Smith',
description: 'Directeur financier, Acme Corp',
},
{
testimonial: 'L\'implémentation SharePoint livrée par Richard a révolutionné nos capacités de gestion documentaire et de collaboration d\'équipe.',
name: 'Nom du client',
description: 'Poste, Entreprise',
testimonial: 'L\'implémentation SharePoint livrée par Richard a complètement transformé la capacité de notre équipe à collaborer et partager des informations. Nous avons constaté une augmentation spectaculaire de la productivité et une réduction significative de l\'encombrement des emails.',
name: 'Jane Doe',
description: 'Chef de projet, Beta Industries',
},
{
testimonial: 'Les connaissances techniques de Richard combinées à sa capacité à comprendre nos besoins commerciaux ont abouti à des solutions qui ont véritablement répondu à nos défis.',
name: 'Nom du client',
description: 'Poste, Entreprise',
testimonial: 'Richard a pris le temps de vraiment comprendre nos défis commerciaux uniques et a développé des solutions IT personnalisées qui répondaient parfaitement à nos besoins. Ses connaissances techniques et ses compétences en résolution de problèmes sont exceptionnelles.',
name: 'David Lee',
description: 'PDG, Gamma Solutions',
},
],
},
callToAction: {
title: 'Prêt à optimiser vos systèmes IT ?',
subtitle: 'Discutons de la façon dont je peux aider votre organisation à rationaliser les processus, améliorer la collaboration et favoriser la transformation numérique.',
button: 'Me contacter',
title: 'Prenez le contrôle de votre avenir IT',
subtitle: 'Discutons de la façon dont nos solutions peuvent rationaliser vos processus, améliorer la collaboration et stimuler la transformation numérique.',
button: 'Planifiez votre consultation maintenant',
},
contact: {
title: 'Prendre contact',
subtitle: 'Vous avez un projet en tête ou des questions sur mes services ? Contactez-moi et commençons une conversation.',
title: 'Contactez notre équipe',
subtitle: 'Discutez de vos besoins d\'entreprise ou renseignez-vous sur nos services professionnels. Nos consultants sont prêts à vous fournir des conseils d\'experts adaptés à vos besoins commerciaux.',
nameLabel: 'Nom',
namePlaceholder: 'Votre nom',
emailLabel: 'Email',
@@ -1263,7 +1318,7 @@ export const translations: Record<string, Translation> = {
messageLabel: 'Message',
messagePlaceholder: 'Votre message',
disclaimer: 'En soumettant ce formulaire, vous acceptez notre politique de confidentialité et nous autorisez à utiliser vos informations pour vous contacter au sujet de nos services.',
description: 'Je répondrai à votre message dès que possible. Vous pouvez également me contacter sur LinkedIn ou GitHub.',
description: 'Toutes les demandes reçoivent une réponse professionnelle rapide. Pour plus d\'informations sur nos solutions d\'entreprise, connectez-vous avec notre équipe sur LinkedIn ou explorez nos ressources techniques sur GitHub.',
},
},
resume: {

View File

@@ -13,6 +13,7 @@ import Analytics from '~/components/common/Analytics.astro';
import BasicScripts from '~/components/common/BasicScripts.astro';
import StructuredData from '~/components/common/StructuredData.astro';
import LanguagePersistence from '~/components/LanguagePersistence.astro';
import GlobalBackground from '~/components/ui/GlobalBackground.astro';
// Comment the line below to disable View Transitions
import { ClientRouter } from 'astro:transitions';
@@ -56,6 +57,7 @@ const { language, textDirection } = I18N;
</head>
<body class="antialiased text-default bg-page tracking-tight">
<GlobalBackground />
<slot name="structured-data" />
<slot />

View File

@@ -18,10 +18,10 @@ export const getHeaderData = (lang = 'en') => {
href: getPermalink('/', 'page', lang),
},
{
text: t.homepage?.services?.tagline || 'Services',
text: t.navigation.services,
href: homeHashLink('#services'),
},
{ text: t.homepage?.contact?.title || 'Contact', href: homeHashLink('#contact') },
{ text: t.navigation.contact, href: homeHashLink('#contact') },
{
text: t.metadata?.aboutUs || 'About Me',
links: [
@@ -37,14 +37,16 @@ export const getHeaderData = (lang = 'en') => {
};
};
// For backward compatibility
export const headerData = getHeaderData();
// For backward compatibility - but don't use this directly, always use getHeaderData(lang) to ensure translations
export const headerData = (lang = 'en') => getHeaderData(lang);
export const getFooterData = (lang = 'en') => {
const t = getTranslation(lang);
return {
secondaryLinks: [
{ text: 'Terms', href: getPermalink('/terms', 'page', lang) },
{ text: 'Privacy Policy', href: getPermalink('/privacy', 'page', lang) },
{ text: t.footer.terms, href: getPermalink('/terms', 'page', lang) },
{ text: t.footer.privacyPolicy, href: getPermalink('/privacy', 'page', lang) },
],
socialLinks: [
{ ariaLabel: 'LinkedIn', icon: 'tabler:brand-linkedin', href: 'https://www.linkedin.com/in/rrpbergsma' },

View File

@@ -1,5 +1,5 @@
---
export const prerender = false;
export const prerender = true;
import Layout from '~/layouts/PageLayout.astro';
import StructuredData from '~/components/common/StructuredData.astro';
import Hero from '~/components/widgets/Hero.astro';
@@ -58,6 +58,7 @@ const metadata = {
<Hero
id="hero"
title="About Me"
isDark={false}
>
<Fragment slot="subtitle">
<strong class="text-3xl md:text-4xl">{t.hero.greeting}</strong><br /><br />{t.hero.subtitle}
@@ -84,7 +85,7 @@ const metadata = {
</Fragment>
<Fragment slot="bg">
<div class="absolute inset-0 bg-blue-50 dark:bg-transparent"></div>
<div class="absolute inset-0 backdrop-blur-sm bg-white/5 dark:bg-gray-900/10"></div>
</Fragment>
</Content>

View File

@@ -1,5 +1,5 @@
---
export const prerender = false;
export const prerender = true;
import Layout from '~/layouts/PageLayout.astro';
import Hero from '~/components/widgets/Hero2.astro';
import Features from '~/components/widgets/Features.astro';
@@ -33,6 +33,7 @@ const metadata = {
tagline={t.navigation.home}
title={t.hero.title}
subtitle={t.hero.subtitle}
isDark={false}
actions={[
{
variant: 'primary',
@@ -54,7 +55,7 @@ const metadata = {
tagline={t.homepage?.services?.tagline || "Services"}
title={t.homepage?.services?.title || "How I Can Help Your Organization"}
subtitle={t.homepage?.services?.subtitle || "I offer a range of specialized IT services to help businesses optimize their operations and digital infrastructure."}
items={t.homepage?.services?.items || [
items={(t.homepage?.services?.items || [
{
title: 'Workflow Automation',
description:
@@ -91,7 +92,7 @@ const metadata = {
'Manage global IT infrastructures, including servers, networks, and end-user devices to ensure reliable operations.',
icon: 'tabler:server',
},
].map(item => ({...item, icon: item.icon || 'tabler:check'}))}
]).map(item => ({...item, icon: item.icon || 'tabler:check'}))}
/>
<!-- Content Widget -->
@@ -215,13 +216,13 @@ const metadata = {
/>
<div class="flex justify-center space-x-4 mt-8 mb-12">
<a href="https://www.linkedin.com/in/rrpbergsma" class="text-gray-500 hover:text-blue-600" target="_blank" rel="noopener noreferrer">
<a href="https://www.linkedin.com/in/rrpbergsma" class="p-3 rounded-full backdrop-blur-sm bg-white/15 dark:bg-transparent border border-gray-200 dark:border-slate-600 hover:bg-white/25 dark:hover:backdrop-blur-md transition-all duration-300 ease-in-out text-primary dark:text-blue-300 hover:shadow-lg" target="_blank" rel="noopener noreferrer">
<span class="sr-only">LinkedIn</span>
<svg class="h-8 w-8" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.454C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.225 0z"/>
</svg>
</a>
<a href="https://github.com/rrpbergsma" class="text-gray-500 hover:text-gray-900 dark:hover:text-white" target="_blank" rel="noopener noreferrer">
<a href="https://github.com/rrpbergsma" class="p-3 rounded-full backdrop-blur-sm bg-white/15 dark:bg-transparent border border-gray-200 dark:border-slate-600 hover:bg-white/25 dark:hover:backdrop-blur-md transition-all duration-300 ease-in-out text-primary dark:text-blue-300 hover:shadow-lg" target="_blank" rel="noopener noreferrer">
<span class="sr-only">GitHub</span>
<svg class="h-8 w-8" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"/>

2
src/types.d.ts vendored
View File

@@ -216,7 +216,7 @@ export interface Form {
}
// WIDGETS
export interface Hero extends Omit<Headline, 'classes'>, Omit<Widget, 'isDark' | 'classes'> {
export interface Hero extends Omit<Headline, 'classes'>, Omit<Widget, 'classes'> {
content?: string;
actions?: string | CallToAction[];
image?: string | unknown;

View File

@@ -11,9 +11,8 @@ const {
SMTP_PORT = '587',
SMTP_USER = '',
SMTP_PASS = '',
ADMIN_EMAIL = 'richard@bergsma.it',
WEBSITE_NAME = 'bergsma.it',
NODE_ENV = 'production'
ADMIN_EMAIL = '',
WEBSITE_NAME = 'bergsma.it'
} = process.env;
// Email configuration
@@ -32,15 +31,6 @@ function initializeTransporter() {
// ProtonMail often requires using their Bridge application for SMTP
const isProtonMail = SMTP_HOST.includes('protonmail');
// Log the email configuration
console.log('Initializing email transporter with:');
console.log(`SMTP Host: ${SMTP_HOST}`);
console.log(`SMTP Port: ${SMTP_PORT}`);
console.log(`SMTP User: ${SMTP_USER}`);
console.log(`Admin Email: ${ADMIN_EMAIL}`);
console.log(`Website Name: ${WEBSITE_NAME}`);
console.log(`Environment: ${NODE_ENV}`);
transporter = nodemailer.createTransport({
host: SMTP_HOST,
port: parseInt(SMTP_PORT, 10),
@@ -57,8 +47,7 @@ function initializeTransporter() {
// Specific ciphers for ProtonMail
ciphers: 'SSLv3'
}
}),
debug: true, // Enable debug output for troubleshooting
})
});
// Verify SMTP connection configuration
@@ -179,12 +168,8 @@ export async function sendEmail(
html: string,
text: string
): Promise<boolean> {
console.log(`Attempting to send email to: ${to}`);
console.log(`Subject: ${subject}`);
// Initialize transporter if not already done
if (!transporter) {
console.log('Initializing transporter');
initializeTransporter();
}
@@ -202,39 +187,8 @@ export async function sendEmail(
text,
};
console.log('Mail options:', {
from: fromAddress,
to,
subject,
textLength: text.length,
htmlLength: html.length
});
await transporter.sendMail(mailOptions);
console.log('Sending email via transporter');
const info = await transporter.sendMail(mailOptions);
console.log('Email sent, info:', info.messageId);
// Log additional information in production mode
if (isProduction) {
console.log('Email delivery details:', {
messageId: info.messageId,
response: info.response,
envelope: info.envelope
});
}
if (!isProduction) {
// In development, log the email content
console.log('Email sent (development mode):');
console.log('To:', to);
console.log('Subject:', subject);
console.log('Preview:', nodemailer.getTestMessageUrl(info));
if (info.message) {
// For stream transport, we can get the message content
console.log('Message:', info.message.toString());
}
}
logEmailAttempt(true, to, subject);
return true;
@@ -273,8 +227,6 @@ export async function sendAdminNotification(
ipAddress?: string,
userAgent?: string
): Promise<boolean> {
console.log('sendAdminNotification called with:', { name, email, messageLength: message.length });
// Validate inputs
if (!name || name.trim() === '') {
console.error('Cannot send admin notification: name is empty');
@@ -313,22 +265,10 @@ export async function sendAdminNotification(
userAgent,
};
console.log('Generating admin notification email content');
const subject = getAdminNotificationSubject();
const html = getAdminNotificationHtml(props);
const text = getAdminNotificationText(props);
console.log(`Sending admin notification to: ${ADMIN_EMAIL}`);
console.log('Admin email environment variables:', {
SMTP_HOST,
SMTP_PORT,
SMTP_USER,
ADMIN_EMAIL,
WEBSITE_NAME,
NODE_ENV,
isProduction
});
// Add a backup email address to ensure delivery
const recipients = ADMIN_EMAIL;
// Uncomment and modify the line below to add a backup email address
@@ -343,8 +283,6 @@ export async function sendUserConfirmation(
email: string,
message: string
): Promise<boolean> {
console.log('sendUserConfirmation called with:', { name, email, messageLength: message.length });
if (!email || email.trim() === '') {
console.error('Cannot send user confirmation: email is empty');
return false;
@@ -367,24 +305,16 @@ export async function sendUserConfirmation(
contactEmail: ADMIN_EMAIL,
};
console.log('Generating user confirmation email content');
const subject = getUserConfirmationSubject(WEBSITE_NAME);
const html = getUserConfirmationHtml(props);
const text = getUserConfirmationText(props);
console.log(`Sending user confirmation to: ${email}`);
return sendEmail(email, subject, html, text);
}
// Initialize the email system
export function initializeEmailSystem(): void {
initializeTransporter();
// Log initialization
console.log(`Email system initialized in ${isProduction ? 'production' : 'development'} mode`);
if (!isProduction) {
console.log('Emails will be logged to console instead of being sent');
}
}
// Initialize on import
@@ -393,7 +323,6 @@ initializeEmailSystem();
// Test email function to verify configuration
export async function testEmailConfiguration(): Promise<boolean> {
if (!isProduction) {
console.log('Email testing skipped in development mode');
return true;
}
@@ -403,20 +332,12 @@ export async function testEmailConfiguration(): Promise<boolean> {
initializeTransporter();
}
console.log('Testing email configuration...');
console.log(`SMTP Host: ${SMTP_HOST}`);
console.log(`SMTP Port: ${SMTP_PORT}`);
console.log(`SMTP User: ${SMTP_USER}`);
console.log(`From Email: ${ADMIN_EMAIL}`);
// Verify connection to SMTP server
const connectionResult = await new Promise<boolean>((resolve) => {
transporter.verify(function(error, _success) {
if (error) {
console.error('SMTP connection test failed:', error);
resolve(false);
} else {
console.log('SMTP connection successful');
resolve(true);
}
});
@@ -426,25 +347,13 @@ export async function testEmailConfiguration(): Promise<boolean> {
return false;
}
console.log('Email configuration test completed successfully');
return true;
} catch (error) {
console.error('Error testing email configuration:', error);
} catch {
return false;
}
}
// Run a test of the email configuration
if (isProduction) {
testEmailConfiguration().then(success => {
if (success) {
console.log('Email system is properly configured');
} else {
console.error('Email system configuration test failed');
console.log('Note: If you continue to have issues with ProtonMail SMTP:');
console.log('1. Ensure ProtonMail Bridge is installed and running if sending from a desktop/server');
console.log('2. Verify you\'re using an app-specific password, not your main account password');
console.log('3. Check if your server allows outgoing connections on the SMTP port');
}
});
testEmailConfiguration();
}

View File

@@ -36,9 +36,7 @@ export type ImagesOptimizer = (
/* ******* */
const config = {
// FIXME: Use this when image.width is minor than deviceSizes
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
deviceSizes: [
640, // older and lower-end phones
750, // iPhone 6-8
@@ -199,12 +197,15 @@ const getBreakpoints = ({
return [width, doubleWidth];
}
if (layout === 'constrained') {
// Use imageSizes when width is smaller than the smallest deviceSize
const sizesToUse = width < config.deviceSizes[0] ? config.imageSizes : config.deviceSizes;
return [
// Always include the image at 1x and 2x the specified width
width,
doubleWidth,
// Filter out any resolutions that are larger than the double-res image
...(breakpoints || config.deviceSizes).filter((w) => w < doubleWidth),
...(breakpoints || sizesToUse).filter((w) => w < doubleWidth),
];
}

View File

@@ -4,7 +4,8 @@
# This script simulates a form submission to the contact form API
API_URL="http://localhost:4321/api/contact"
ADMIN_EMAIL="richard@bergsma.it"
# Get admin email from environment variable or use a placeholder for testing
ADMIN_EMAIL="${ADMIN_EMAIL:-admin@example.com}"
echo "Starting contact form test with curl..."
echo "API URL: $API_URL"

View File

@@ -33,8 +33,7 @@ const transporter = nodemailer.createTransport({
tls: {
ciphers: 'SSLv3',
rejectUnauthorized: false
},
debug: true, // Enable debug output
}
});
// Test the connection