Updated language selector
This commit is contained in:
147
src/components/LanguageDropdown.astro
Normal file
147
src/components/LanguageDropdown.astro
Normal file
@@ -0,0 +1,147 @@
|
||||
---
|
||||
// src/components/LanguageDropdown.astro
|
||||
import { Icon } from 'astro-icon/components';
|
||||
|
||||
interface Props {
|
||||
currentLang: string;
|
||||
}
|
||||
|
||||
const { currentLang } = Astro.props;
|
||||
|
||||
const languages = [
|
||||
{ code: 'en', name: 'English', flag: 'gb' },
|
||||
{ code: 'nl', name: 'Dutch', flag: 'nl' },
|
||||
{ code: 'de', name: 'German', flag: 'de' },
|
||||
];
|
||||
|
||||
const currentLanguage = languages.find(lang => lang.code === currentLang) || languages[0];
|
||||
---
|
||||
|
||||
<div class="relative inline-block text-left">
|
||||
<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 transition-colors duration-200"
|
||||
id="menu-button"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<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="hidden origin-top-right absolute right-0 mt-2 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"
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
aria-labelledby="menu-button"
|
||||
tabindex="-1"
|
||||
id="language-menu"
|
||||
>
|
||||
<div class="py-1" role="none">
|
||||
{languages.map(lang => (
|
||||
<a
|
||||
href={`/${lang.code}`}
|
||||
class="text-gray-700 dark:text-gray-300 block 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"
|
||||
>
|
||||
<Icon name={`circle-flags:${lang.flag}`} class="inline-block w-5 h-5 mr-2" />
|
||||
{lang.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#language-menu:not(.hidden) {
|
||||
animation: slideIn 0.2s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-0.5rem);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function setupLanguageDropdown() {
|
||||
const button = document.querySelector<HTMLButtonElement>('#menu-button');
|
||||
const menu = document.querySelector<HTMLDivElement>('#language-menu');
|
||||
const chevronIcon = document.querySelector<HTMLElement>('#chevron-icon');
|
||||
|
||||
if (!button || !menu || !chevronIcon) {
|
||||
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) {
|
||||
menu.classList.remove('hidden');
|
||||
button.setAttribute('aria-expanded', 'true');
|
||||
chevronIcon.style.transform = 'rotate(180deg)';
|
||||
isOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize closed state
|
||||
closeMenu();
|
||||
|
||||
// Toggle menu
|
||||
button.addEventListener('click', (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (isOpen) {
|
||||
closeMenu();
|
||||
} else {
|
||||
openMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Close when clicking outside
|
||||
document.addEventListener('click', (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (isOpen && !menu.contains(target) && !button.contains(target)) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle keyboard navigation
|
||||
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && isOpen) {
|
||||
closeMenu();
|
||||
button.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Run setup when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', setupLanguageDropdown);
|
||||
} else {
|
||||
setupLanguageDropdown();
|
||||
}
|
||||
</script>
|
@@ -1,37 +1,11 @@
|
||||
---
|
||||
// Define your supported languages
|
||||
const languages = [
|
||||
{ code: 'en', label: 'English' },
|
||||
{ code: 'fr', label: 'Français' },
|
||||
{ code: 'es', label: 'Español' },
|
||||
];
|
||||
import LanguageSelectorComponent from './LanguageSelectorReact';
|
||||
|
||||
// Determine the current language from the URL for setting the selected option.
|
||||
const pathSegments = window.location.pathname.split('/').filter(Boolean);
|
||||
const currentLang = pathSegments[0] || 'en';
|
||||
interface Props {
|
||||
defaultLang: string;
|
||||
}
|
||||
|
||||
const { defaultLang } = Astro.props;
|
||||
---
|
||||
<select id="language-selector" class="p-2 border rounded bg-white text-gray-700">
|
||||
{languages.map((lang) => (
|
||||
<option value={lang.code} selected={lang.code === currentLang}>
|
||||
{lang.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<script>
|
||||
// This script runs in the browser
|
||||
const select = document.getElementById('language-selector');
|
||||
select.addEventListener('change', (event) => {
|
||||
const selectedLang = event.target.value;
|
||||
const currentPath = window.location.pathname;
|
||||
const segments = currentPath.split('/').filter(Boolean);
|
||||
const supportedLangs = ['en', 'fr', 'es'];
|
||||
if (supportedLangs.includes(segments[0])) {
|
||||
segments[0] = selectedLang;
|
||||
} else {
|
||||
segments.unshift(selectedLang);
|
||||
}
|
||||
const newPath = '/' + segments.join('/');
|
||||
window.location.href = newPath;
|
||||
});
|
||||
</script>
|
||||
<LanguageSelectorComponent client:load defaultLang={defaultLang} />
|
61
src/components/LanguageSelectorReact.tsx
Normal file
61
src/components/LanguageSelectorReact.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useState } from 'react';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { Icon as IconComponent } from 'astro-icon/components';
|
||||
|
||||
// Create a wrapper component for Icon with proper TypeScript types
|
||||
const Icon = ({ className, ...props }: ComponentProps<typeof IconComponent> & { className?: string }) => {
|
||||
return <IconComponent {...props} class={className} />;
|
||||
};
|
||||
|
||||
interface Language {
|
||||
code: string;
|
||||
name: string;
|
||||
flag: string;
|
||||
}
|
||||
|
||||
const languages: Language[] = [
|
||||
{ code: 'en', name: 'English', flag: 'gb' },
|
||||
{ code: 'nl', name: 'Dutch', flag: 'nl' },
|
||||
{ code: 'de', name: 'German', flag: 'de' },
|
||||
];
|
||||
|
||||
interface LanguageSelectorProps {
|
||||
defaultLang: string;
|
||||
}
|
||||
|
||||
export default function LanguageSelectorComponent({ defaultLang }: LanguageSelectorProps) {
|
||||
const [currentLang] = useState<string>(defaultLang);
|
||||
|
||||
const handleLanguageSelect = (code: string) => {
|
||||
if (code !== currentLang) {
|
||||
window.location.href = `/${code}`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-4">
|
||||
{languages.map((language) => (
|
||||
<button
|
||||
key={language.code}
|
||||
onClick={() => handleLanguageSelect(language.code)}
|
||||
className={`
|
||||
inline-flex items-center px-3 py-2 text-sm font-medium rounded-md
|
||||
transition-colors duration-200 hover:bg-gray-100
|
||||
${language.code === currentLang
|
||||
? 'text-blue-600 bg-blue-50'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}
|
||||
`}
|
||||
aria-current={language.code === currentLang ? 'page' : undefined}
|
||||
>
|
||||
<Icon
|
||||
name={`circle-flags:${language.flag}`}
|
||||
className="w-5 h-5 mr-2"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{language.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -4,11 +4,10 @@ import { Icon } from 'astro-icon/components';
|
||||
import Logo from '~/components/Logo.astro';
|
||||
import ToggleTheme from '~/components/common/ToggleTheme.astro';
|
||||
import ToggleMenu from '~/components/common/ToggleMenu.astro';
|
||||
import Button from '~/components/ui/Button.astro';
|
||||
import LanguageDropdown from '~/components/LanguageDropdown.astro';
|
||||
|
||||
import { getHomePermalink } from '~/utils/permalinks';
|
||||
import { trimSlash, getAsset } from '~/utils/permalinks';
|
||||
import type { CallToAction } from '~/types';
|
||||
|
||||
interface Link {
|
||||
text?: string;
|
||||
@@ -24,7 +23,6 @@ interface MenuLink extends Link {
|
||||
export interface Props {
|
||||
id?: string;
|
||||
links?: Array<MenuLink>;
|
||||
actions?: Array<CallToAction>;
|
||||
isSticky?: boolean;
|
||||
isDark?: boolean;
|
||||
isFullWidth?: boolean;
|
||||
@@ -36,7 +34,6 @@ export interface Props {
|
||||
const {
|
||||
id = 'header',
|
||||
links = [],
|
||||
actions = [],
|
||||
isSticky = false,
|
||||
isDark = false,
|
||||
isFullWidth = false,
|
||||
@@ -62,7 +59,7 @@ const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`;
|
||||
'relative text-default py-3 px-3 md:px-6 mx-auto w-full',
|
||||
{
|
||||
'md:flex md:justify-between': position !== 'center',
|
||||
},
|
||||
},
|
||||
{
|
||||
'md:grid md:grid-cols-3 md:items-center': position === 'center',
|
||||
},
|
||||
@@ -150,60 +147,10 @@ const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`;
|
||||
</a>
|
||||
)
|
||||
}
|
||||
<!-- Language Selector as Borderless Buttons -->
|
||||
<div id="language-selector" class="flex space-x-4">
|
||||
<button
|
||||
type="button"
|
||||
data-lang="en"
|
||||
class="dark:text-white:hover:text-blue-500 focus:outline-none"
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-lang="nl"
|
||||
class="dark:text-white:hover:text-blue-500 focus:outline-none"
|
||||
>
|
||||
NL
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-lang="de"
|
||||
class="dark:text-white:hover:text-blue-500 focus:outline-none"
|
||||
>
|
||||
DE
|
||||
</button>
|
||||
</div>
|
||||
<!-- Language Selector as Select Element -->
|
||||
<LanguageDropdown currentLang={currentPath.split('/')[1] || 'en'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script client:load>
|
||||
// Define supported languages
|
||||
const supportedLangs = ['en', 'nl', 'de'];
|
||||
// Split current URL path into segments
|
||||
let segments = window.location.pathname.split('/').filter(Boolean);
|
||||
// Determine current language based on URL, defaulting to 'en'
|
||||
const currentLang = supportedLangs.includes(segments[0]) ? segments[0] : 'en';
|
||||
|
||||
// Add active styling and event listeners to language buttons
|
||||
document.querySelectorAll('#language-selector button').forEach((button) => {
|
||||
if (button.getAttribute('data-lang') === currentLang) {
|
||||
button.classList.add('font-bold', 'text-blue-500');
|
||||
}
|
||||
button.addEventListener('click', () => {
|
||||
// Re-read the URL segments in case the path has changed
|
||||
segments = window.location.pathname.split('/').filter(Boolean);
|
||||
const selectedLang = button.getAttribute('data-lang');
|
||||
if (supportedLangs.includes(segments[0])) {
|
||||
segments[0] = selectedLang;
|
||||
} else {
|
||||
segments.unshift(selectedLang);
|
||||
}
|
||||
// Rebuild the URL and redirect
|
||||
const newPath = '/' + segments.join('/');
|
||||
window.location.href = newPath;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</header>
|
Reference in New Issue
Block a user