NebulaCloud/components/FloatingLanguageSwitcher.tsx

272 lines
12 KiB
TypeScript
Raw Permalink Normal View History

2025-09-15 17:28:58 +08:00
'use client';
import { useState, useEffect, useRef } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { Locale, locales, localeNames, localeFlags } from '../lib/i18n';
interface FloatingLanguageSwitcherProps {
locale: Locale;
}
export default function FloatingLanguageSwitcher({ locale }: FloatingLanguageSwitcherProps) {
const [isOpen, setIsOpen] = useState(false);
const [isVisible, setIsVisible] = useState(true);
const [isMounted, setIsMounted] = useState(false);
const router = useRouter();
const pathname = usePathname();
const switcherRef = useRef<HTMLDivElement>(null);
const lastScrollY = useRef(0);
// Ensure component is mounted before rendering to avoid hydration issues
useEffect(() => {
setIsMounted(true);
}, []);
// Handle scroll to show/hide the switcher (temporarily disabled for visibility)
useEffect(() => {
// Keep the switcher always visible for now
setIsVisible(true);
// Uncomment below to re-enable scroll hiding
/*
const handleScroll = () => {
const currentScrollY = window.scrollY;
if (currentScrollY > lastScrollY.current && currentScrollY > 100) {
// Scrolling down
setIsVisible(false);
} else {
// Scrolling up
setIsVisible(true);
}
lastScrollY.current = currentScrollY;
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
*/
}, []);
// Close menu when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
// Add a small delay to ensure button click is processed first
setTimeout(() => {
if (switcherRef.current && !switcherRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}, 10);
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen]);
const switchLanguage = (newLocale: Locale) => {
console.log('Switching language from', locale, 'to', newLocale);
console.log('Current pathname:', pathname);
try {
// Get the current path segments
const pathSegments = pathname.split('/').filter(Boolean);
console.log('Path segments:', pathSegments);
// Remove the current locale from the beginning if it exists
if (pathSegments[0] && locales.includes(pathSegments[0] as Locale)) {
pathSegments.shift();
}
// Construct the new path with the new locale
const newPath = `/${newLocale}${pathSegments.length > 0 ? '/' + pathSegments.join('/') : ''}`;
console.log('New path:', newPath);
// Use replace instead of push to avoid history issues
router.replace(newPath);
setIsOpen(false);
} catch (error) {
console.error('Error switching language:', error);
// Fallback to simple navigation
router.replace(`/${newLocale}`);
setIsOpen(false);
}
};
// Don't render until mounted to avoid hydration issues
if (!isMounted) {
return null;
}
return (
<div
ref={switcherRef}
className={`fixed right-4 md:right-6 top-1/2 -translate-y-1/2 transition-all duration-300 ${
isVisible ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0'
}`}
style={{
pointerEvents: 'auto',
isolation: 'isolate',
zIndex: 999999,
position: 'fixed',
}}
data-oid="taerglg"
>
{/* Main Switcher Button */}
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
console.log('Button clicked, current isOpen:', isOpen);
const newState = !isOpen;
setIsOpen(newState);
console.log('Setting isOpen to:', newState);
}}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
className="group relative bg-slate-800/90 border-2 border-green-400/50 rounded-full p-3 shadow-2xl hover:bg-slate-700/90 hover:border-green-400 transition-all duration-200 cursor-pointer"
title={`Current: ${localeNames[locale]}`}
type="button"
style={{
pointerEvents: 'auto',
touchAction: 'manipulation',
zIndex: 1000000,
position: 'relative',
}}
data-oid="_:fx8.e"
>
<div className="flex items-center justify-center w-8 h-8" data-oid="di.5x8v">
<span className="text-2xl" data-oid="ll7ebjo">
{localeFlags[locale]}
</span>
</div>
{/* Tooltip */}
<div
className="absolute right-full mr-3 top-1/2 -translate-y-1/2 bg-slate-800/95 backdrop-blur-md text-white px-3 py-2 rounded-lg text-sm font-medium opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none whitespace-nowrap"
data-oid=":kya.x4"
>
{localeNames[locale]}
<div
className="absolute left-full top-1/2 -translate-y-1/2 border-4 border-transparent border-l-slate-800/95"
data-oid="01-w6mc"
></div>
</div>
</button>
{/* Language Options */}
{isOpen && (
<div
className="absolute right-full mr-3 md:mr-4 top-1/2 -translate-y-1/2 bg-slate-800/98 rounded-2xl shadow-2xl border-2 border-green-400/30 overflow-hidden min-w-[180px] md:min-w-[200px]"
style={{
zIndex: 1000001,
pointerEvents: 'auto',
}}
data-oid="e1v:81b"
>
<div className="p-2" data-oid="xcuqg9t">
<div
className="px-3 py-2 text-xs font-medium text-white/60 uppercase tracking-wider border-b border-white/10 mb-2"
data-oid="_jrtjyl"
>
Select Language
</div>
<div className="space-y-1" data-oid="tg0y3c4">
{locales.map((loc) => (
<button
key={loc}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
console.log('Language option clicked:', loc);
// Add immediate feedback
e.currentTarget.style.backgroundColor =
'rgba(34, 197, 94, 0.3)';
setTimeout(() => {
switchLanguage(loc);
}, 100);
}}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
className={`w-full text-left px-3 py-3 rounded-xl flex items-center space-x-3 transition-all duration-200 cursor-pointer ${
locale === loc
? 'bg-green-500/30 text-green-400 border border-green-500/50'
: 'text-white hover:bg-green-500/20 hover:text-green-400'
}`}
type="button"
style={{
pointerEvents: 'auto',
touchAction: 'manipulation',
zIndex: 1000002,
}}
data-oid="i3cq3gh"
>
<span className="text-lg" data-oid="kl5sqzg">
{localeFlags[loc]}
</span>
<div className="flex-1" data-oid="ump7m:6">
<span className="font-medium" data-oid="lsnf7ck">
{localeNames[loc]}
</span>
{locale === loc && (
<div
className="text-xs text-green-400/70 mt-0.5"
data-oid="6e9d:gj"
>
Current
</div>
)}
</div>
{locale === loc && (
<svg
className="w-4 h-4 text-green-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
data-oid="v2bjs6b"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
data-oid="mmqle:5"
/>
</svg>
)}
</button>
))}
</div>
</div>
{/* Arrow pointing to the button */}
<div
className="absolute left-full top-1/2 -translate-y-1/2 border-8 border-transparent border-l-slate-800/95"
data-oid="0gukon1"
></div>
</div>
)}
{/* Subtle glow effect */}
<div
className="absolute inset-0 rounded-full bg-green-400/20 animate-pulse pointer-events-none"
style={{ zIndex: -1 }}
data-oid="g55eh56"
></div>
{/* Floating indicator */}
<div
className="absolute -top-1 -right-1 w-3 h-3 bg-green-400 rounded-full animate-bounce pointer-events-none"
style={{ zIndex: -1 }}
data-oid="1t6b6v9"
></div>
</div>
);
}