- Enhanced the LanguageDropdown component by updating language names to their native forms and adding non-breaking spaces for better readability. - Refactored the Header component to improve layout, including a gap between icons and adjusted class attributes for better spacing. - Ensured consistent filtering of navigation links and improved accessibility for the contact link.
399 lines
14 KiB
Plaintext
399 lines
14 KiB
Plaintext
---
|
|
import { Icon } from 'astro-icon/components';
|
|
import { supportedLanguages } from '~/i18n/translations';
|
|
|
|
interface Props {
|
|
currentLang: string;
|
|
}
|
|
|
|
const { currentLang } = Astro.props;
|
|
|
|
type SupportedLanguage = (typeof supportedLanguages)[number];
|
|
|
|
const languages = [
|
|
{ code: 'en' as SupportedLanguage, name: 'English\u00A0\u00A0', flag: 'gb' },
|
|
{ code: 'nl' as SupportedLanguage, name: 'Nederlands', flag: 'nl' },
|
|
{ code: 'de' as SupportedLanguage, name: 'Deutsch', flag: 'de' },
|
|
{ code: 'fr' as SupportedLanguage, name: 'Français', flag: 'fr' },
|
|
].filter((lang) => supportedLanguages.includes(lang.code));
|
|
|
|
const currentLanguage = languages.find((lang) => lang.code === currentLang) || languages[0];
|
|
---
|
|
|
|
<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 dropdown-button"
|
|
id="menu-button"
|
|
aria-expanded="false"
|
|
aria-haspopup="true"
|
|
aria-label={`Select language. Current language: ${currentLanguage.name}`}
|
|
>
|
|
<Icon name={`circle-flags:${currentLanguage.flag}`} class="inline-block w-5 h-5 mr-2" />
|
|
<span id="selected-language">{currentLanguage.name}</span>
|
|
<Icon
|
|
name="tabler:chevron-down"
|
|
class="ml-2 -mr-1 h-5 w-5 transition-transform duration-200"
|
|
aria-hidden="true"
|
|
id="chevron-icon"
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
<div
|
|
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"
|
|
tabindex="-1"
|
|
id="language-menu"
|
|
style="max-height: min(300px, 70vh);"
|
|
>
|
|
<div class="py-1" role="none">
|
|
{
|
|
languages.map((lang) => (
|
|
<button
|
|
type="button"
|
|
data-lang-code={lang.code}
|
|
class="text-gray-700 dark:text-gray-300 block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white transition-colors duration-200"
|
|
role="menuitem"
|
|
tabindex="-1"
|
|
aria-label={`Switch to ${lang.name} language`}
|
|
>
|
|
<Icon name={`circle-flags:${lang.flag}`} class="inline-block w-5 h-5 mr-2" />
|
|
{lang.name}
|
|
</button>
|
|
))
|
|
}
|
|
</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-dropdown {
|
|
@apply md:inline-block md:relative;
|
|
}
|
|
|
|
.language-select {
|
|
display: none;
|
|
}
|
|
|
|
@media (max-width: 767px) {
|
|
.language-dropdown {
|
|
display: inline-block;
|
|
}
|
|
|
|
/* Keep the native select hidden even on mobile */
|
|
.language-select {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
.language-menu.open-downward {
|
|
top: 100%;
|
|
margin-top: 0.5rem;
|
|
transform-origin: top;
|
|
}
|
|
|
|
.language-menu.open-upward {
|
|
bottom: 100%;
|
|
margin-bottom: 0.5rem;
|
|
transform-origin: bottom;
|
|
}
|
|
|
|
.language-menu:not(.hidden).open-downward {
|
|
animation: slideDown 0.2s ease-out forwards;
|
|
}
|
|
|
|
.language-menu:not(.hidden).open-upward {
|
|
animation: slideUp 0.2s ease-out forwards;
|
|
}
|
|
|
|
@keyframes slideDown {
|
|
from {
|
|
opacity: 0;
|
|
transform: scale(0.95) translateY(-0.5rem);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: scale(1) translateY(0);
|
|
}
|
|
}
|
|
|
|
@keyframes slideUp {
|
|
from {
|
|
opacity: 0;
|
|
transform: scale(0.95) translateY(0.5rem);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: scale(1) translateY(0);
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script define:vars={{ supportedLanguages }}>
|
|
function setupLanguageDropdown() {
|
|
const button = document.querySelector('#menu-button');
|
|
const menu = document.querySelector('#language-menu');
|
|
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 || !languageSelect) {
|
|
return;
|
|
}
|
|
|
|
let isOpen = false;
|
|
|
|
function closeMenu() {
|
|
if (menu && button && chevronIcon) {
|
|
menu.classList.add('hidden');
|
|
button.setAttribute('aria-expanded', 'false');
|
|
chevronIcon.style.transform = 'rotate(0deg)';
|
|
isOpen = false;
|
|
}
|
|
}
|
|
|
|
function openMenu() {
|
|
if (menu && button && chevronIcon) {
|
|
// First show the menu to calculate its height
|
|
menu.classList.remove('hidden');
|
|
menu.classList.remove('open-upward', 'open-downward');
|
|
|
|
// Calculate available space
|
|
const buttonRect = button.getBoundingClientRect();
|
|
const menuRect = menu.getBoundingClientRect();
|
|
const viewportHeight = window.innerHeight;
|
|
const spaceBelow = viewportHeight - buttonRect.bottom;
|
|
const spaceAbove = buttonRect.top;
|
|
const menuHeight = Math.min(menuRect.height, 300); // Cap at 300px
|
|
|
|
// Determine if menu should open upward
|
|
const shouldOpenUpward = spaceBelow < menuHeight && spaceAbove > spaceBelow;
|
|
|
|
// Position menu
|
|
menu.style.maxHeight = `${Math.min(shouldOpenUpward ? spaceAbove - 8 : spaceBelow - 8, 300)}px`;
|
|
|
|
if (shouldOpenUpward) {
|
|
menu.classList.add('open-upward');
|
|
chevronIcon.style.transform = 'rotate(180deg)';
|
|
} else {
|
|
menu.classList.add('open-downward');
|
|
chevronIcon.style.transform = 'rotate(0deg)';
|
|
}
|
|
|
|
button.setAttribute('aria-expanded', 'true');
|
|
isOpen = true;
|
|
}
|
|
}
|
|
|
|
// Initialize closed state
|
|
closeMenu();
|
|
|
|
// Toggle menu
|
|
button.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
if (isOpen) {
|
|
closeMenu();
|
|
} else {
|
|
openMenu();
|
|
// Don't automatically focus any menu item to avoid default highlighting
|
|
}
|
|
});
|
|
|
|
// Handle language selection
|
|
languageButtons.forEach((langButton) => {
|
|
langButton.addEventListener('click', () => {
|
|
const langCode = langButton.dataset.langCode;
|
|
if (!langCode) return;
|
|
|
|
// Update button text and icon
|
|
const langName = langButton.textContent ? langButton.textContent.trim() : '';
|
|
const flagIcon = langButton.querySelector('svg');
|
|
if (langName && flagIcon) {
|
|
selectedLanguageText.textContent = langName;
|
|
const currentFlag = button.querySelector('svg:first-child');
|
|
if (currentFlag) {
|
|
currentFlag.replaceWith(flagIcon.cloneNode(true));
|
|
}
|
|
}
|
|
|
|
// Close menu
|
|
closeMenu();
|
|
|
|
// 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('/')}`;
|
|
}
|
|
|
|
// Handle special case for root path
|
|
const isRootPath = pathSegments.length === 0 || (isLangPath && pathSegments.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);
|
|
|
|
// 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();
|
|
});
|
|
});
|
|
|
|
// Handle language selection from select element
|
|
languageSelect.addEventListener('change', (event) => {
|
|
const langCode = event.target.value;
|
|
if (!langCode) return;
|
|
|
|
// 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('/')}`;
|
|
}
|
|
|
|
// Handle special case for root path
|
|
const isRootPath = pathSegments.length === 0 || (isLangPath && pathSegments.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);
|
|
|
|
// 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();
|
|
});
|
|
}
|
|
|
|
// Run setup when DOM is ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', setupLanguageDropdown);
|
|
} else {
|
|
setupLanguageDropdown();
|
|
}
|
|
|
|
// Re-run setup when the page content is updated (e.g., after navigation)
|
|
document.addEventListener('astro:page-load', setupLanguageDropdown);
|
|
|
|
// Listen for popstate events (browser back/forward buttons)
|
|
window.addEventListener('popstate', (_event) => {
|
|
// No need to manually update anything here as the browser will
|
|
// automatically load the correct URL, and Astro will handle the rendering
|
|
});
|
|
</script>
|