managarten/apps/bauntown/apps/landing/src/components/LanguageSwitcher.astro
Till-JS 5b1e12e5d6 feat: add new projects bauntown, presi, voxel-lava, whopixels
- apps/bauntown: Developer community website (Astro landing)
- apps/presi: Presentation project
- games/voxel-lava: Voxel lava game (SvelteKit)
- games/whopixels: Whopixels game

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 15:11:53 +01:00

356 lines
No EOL
9.4 KiB
Text

---
import { languages } from '../i18n/ui';
import { getLangFromUrl } from '../utils/i18n';
const lang = getLangFromUrl(Astro.url);
const currentPath = Astro.url.pathname;
const getPathInLang = (targetLang: string) => {
// Split URL path into segments
const segments = currentPath.split('/').filter(Boolean);
// First segment is always the language code
if (segments.length === 0) {
// Homepage case
return `/${targetLang}`;
} else if (Object.keys(languages).includes(segments[0])) {
// Normal case: first segment is a language code
segments[0] = targetLang;
} else {
// Fallback for any non-language URLs
segments.unshift(targetLang);
}
return `/${segments.join('/')}`;
};
// Props for the component
interface Props {
dropdownStyle?: boolean;
}
const { dropdownStyle = false } = Astro.props;
---
{
dropdownStyle ? (
<div class="language-dropdown">
<button class="dropdown-button">
<span class="current-lang">{languages[lang]}</span>
<svg class="dropdown-arrow" xmlns="http://www.w3.org/2000/svg" width="12" height="6" viewBox="0 0 12 6" fill="none">
<path d="M1 1L6 5L11 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<div class="dropdown-menu">
{Object.entries(languages).map(([code, name]) => (
<a
href={getPathInLang(code)}
class={lang === code ? 'active' : ''}
hreflang={code}
lang={code}
>
{name}
</a>
))}
</div>
</div>
) : (
<div class="language-selector">
{Object.entries(languages).map(([code, name]) => (
<a
href={getPathInLang(code)}
class={lang === code ? 'active' : ''}
hreflang={code}
lang={code}
>
{name}
</a>
))}
</div>
)
}
<style>
/* Shared styles */
.language-selector,
.language-dropdown {
display: flex;
align-items: center;
}
.language-selector a,
.dropdown-menu a {
padding: 0.35rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
color: var(--text-color);
text-decoration: none;
transition: background-color 0.2s, color 0.2s;
}
.language-selector a:hover,
.dropdown-menu a:hover {
background-color: var(--hover-bg);
}
.language-selector a.active,
.dropdown-menu a.active {
font-weight: bold;
background-color: var(--hover-bg);
}
/* Horizontal selector styles */
.language-selector {
gap: 0.25rem;
}
/* Dropdown styles */
.language-dropdown {
position: relative;
z-index: 150;
}
.dropdown-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: none;
border: 1px solid var(--border-color);
border-radius: 0.375rem;
font-size: 0.875rem;
color: var(--text-color);
cursor: pointer;
transition: background-color 0.2s;
}
/* Fix text color in dark mode */
:global(:root.dark) .dropdown-button {
color: #f9fafb;
}
.dropdown-button:hover {
background-color: var(--hover-bg);
}
.dropdown-arrow {
transition: transform 0.2s;
}
.dropdown-menu {
position: absolute;
top: calc(100% + 0.25rem);
right: 0;
display: flex;
flex-direction: column;
min-width: 120px;
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 0.375rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
opacity: 0;
transform: translateY(-10px);
visibility: hidden;
transition: opacity 0.2s, transform 0.2s, visibility 0s 0.2s;
z-index: 200; /* Higher z-index to ensure it appears above other elements */
overflow: hidden;
}
/* Initial state for dropdowns */
.dropdown-menu {
opacity: 0;
visibility: hidden;
}
.dropdown-menu.show {
opacity: 1;
transform: translateY(0);
visibility: visible;
transition: opacity 0.2s, transform 0.2s;
}
.dropdown-menu a {
padding: 0.75rem 1rem;
border-radius: 0;
width: 100%;
text-align: left;
}
/* Fix link color in dark mode */
:global(:root.dark) .dropdown-menu a {
color: #f9fafb;
}
/* Only show dropdown on desktop hover, not mobile */
@media (min-width: 769px) {
.language-dropdown:hover .dropdown-menu,
.dropdown-button:focus-within .dropdown-menu {
opacity: 1;
transform: translateY(0);
visibility: visible;
transition: opacity 0.2s, transform 0.2s;
}
.language-dropdown:hover .dropdown-arrow,
.dropdown-button:focus-within .dropdown-arrow,
.dropdown-button[aria-expanded="true"] .dropdown-arrow {
transform: rotate(180deg);
}
}
/* Show dropdown in mobile when active */
.dropdown-menu.show {
opacity: 1;
transform: translateY(0);
visibility: visible;
transition: opacity 0.2s, transform 0.2s;
}
/* Standard animation for all dropdowns */
.mobile-menu-controls .dropdown-menu.show {
transform: none !important;
opacity: 1;
visibility: visible;
}
/* Mobile styles */
@media (max-width: 768px) {
.language-selector {
flex-direction: row;
width: 100%;
justify-content: flex-start;
gap: 1rem;
}
.language-selector a {
font-size: 1rem;
padding: 0.5rem 0.75rem;
}
.language-dropdown {
width: 100%;
position: relative; /* Keep relative positioning for proper dropdown placement */
}
.dropdown-button {
width: 100%;
justify-content: space-between;
padding: 1rem 1.25rem;
font-size: 1.1rem;
text-align: center;
}
/* Important: absolute positioning for mobile dropdown */
.dropdown-menu {
position: absolute;
width: 100%;
left: 0;
top: 100%; /* Show below the button */
z-index: 2000; /* Extra high z-index */
max-height: calc(100vh - 200px);
overflow-y: auto;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
/* Style for mobile menu dropdown - directly attached to button */
.mobile-menu-controls .dropdown-menu {
position: absolute;
width: 100%; /* Match button width */
left: 0;
top: 100%; /* Position below the button */
bottom: auto;
margin-top: 2px; /* Small gap */
max-height: 180px;
border-radius: 8px;
z-index: 2000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
background-color: var(--card-bg);
}
.dropdown-menu a {
padding: 1rem;
font-size: 1rem;
}
}
</style>
<script>
// For accessibility - allow keyboard navigation
document.addEventListener('DOMContentLoaded', () => {
// Handle all dropdown buttons in the document, not just the first one
const dropdownButtons = document.querySelectorAll('.dropdown-button');
dropdownButtons.forEach(dropdownButton => {
if (!dropdownButton) return;
// Open/close dropdown on click
dropdownButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const isExpanded = dropdownButton.getAttribute('aria-expanded') === 'true';
dropdownButton.setAttribute('aria-expanded', isExpanded ? 'false' : 'true');
const menu = dropdownButton.nextElementSibling;
if (menu && menu.classList.contains('dropdown-menu')) {
menu.classList.toggle('show');
if (!isExpanded) {
// Focus the first menu item
const firstItem = menu.querySelector('a');
if (firstItem) firstItem.focus();
}
}
});
dropdownButton.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
dropdownButton.click();
}
// Close when escape is pressed
if (e.key === 'Escape') {
const menu = dropdownButton.nextElementSibling;
if (menu && menu.classList.contains('show')) {
menu.classList.remove('show');
dropdownButton.setAttribute('aria-expanded', 'false');
dropdownButton.focus();
}
}
});
// Initialize ARIA attributes
dropdownButton.setAttribute('aria-haspopup', 'true');
dropdownButton.setAttribute('aria-expanded', 'false');
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
dropdownButtons.forEach(dropdownButton => {
if (!dropdownButton.contains(e.target)) {
const menu = dropdownButton.nextElementSibling;
if (menu && menu.classList.contains('show')) {
menu.classList.remove('show');
dropdownButton.setAttribute('aria-expanded', 'false');
}
}
});
});
// Add click listener to all dropdown menu items
const dropdownMenuItems = document.querySelectorAll('.dropdown-menu a');
dropdownMenuItems.forEach(item => {
item.addEventListener('click', (e) => {
const menu = item.closest('.dropdown-menu');
if (menu) {
menu.classList.remove('show');
const button = menu.previousElementSibling;
if (button) {
button.setAttribute('aria-expanded', 'false');
}
}
});
});
});
</script>