- Added StripePaymentForm component for handling donations with Stripe integration. - Included customer information fields and payment processing logic. - Integrated language switcher component for multilingual support. - Configured i18n with multiple languages and corresponding translation files. - Added translation files for Arabic, German, English, Spanish, French, Portuguese, Russian, and Chinese.
226 lines
7.7 KiB
TypeScript
226 lines
7.7 KiB
TypeScript
import { useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { motion, AnimatePresence } from 'framer-motion'
|
|
import { Globe, ChevronDown } from 'lucide-react'
|
|
import { languages } from '@/i18n/config'
|
|
|
|
interface LanguageSwitcherProps {
|
|
className?: string
|
|
variant?: 'default' | 'minimal' | 'mobile'
|
|
}
|
|
|
|
export function LanguageSwitcher({
|
|
className = '',
|
|
variant = 'default'
|
|
}: LanguageSwitcherProps) {
|
|
const { i18n, t } = useTranslation()
|
|
const [isOpen, setIsOpen] = useState(false)
|
|
const currentLang = i18n.language || 'en'
|
|
const currentLanguage = languages[currentLang as keyof typeof languages]
|
|
|
|
const handleLanguageChange = (langCode: string) => {
|
|
i18n.changeLanguage(langCode)
|
|
setIsOpen(false)
|
|
|
|
// Update document direction for RTL languages
|
|
const langConfig = languages[langCode as keyof typeof languages]
|
|
document.documentElement.dir = langConfig.dir
|
|
document.documentElement.lang = langCode
|
|
|
|
// Store preference
|
|
localStorage.setItem('i18nextLng', langCode)
|
|
}
|
|
|
|
const dropdownVariants = {
|
|
hidden: {
|
|
opacity: 0,
|
|
scale: 0.95,
|
|
y: -10
|
|
},
|
|
visible: {
|
|
opacity: 1,
|
|
scale: 1,
|
|
y: 0,
|
|
transition: {
|
|
duration: 0.2,
|
|
ease: 'easeOut'
|
|
}
|
|
},
|
|
exit: {
|
|
opacity: 0,
|
|
scale: 0.95,
|
|
y: -10,
|
|
transition: {
|
|
duration: 0.15,
|
|
ease: 'easeIn'
|
|
}
|
|
}
|
|
}
|
|
|
|
if (variant === 'minimal') {
|
|
return (
|
|
<div className={`relative ${className}`}>
|
|
<button
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className="flex items-center gap-1 p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
|
aria-label={t('accessibility.toggleLanguage')}
|
|
>
|
|
<span className="text-lg">{currentLanguage?.flag || '🌐'}</span>
|
|
<span className="text-sm font-medium">{currentLang.toUpperCase()}</span>
|
|
</button>
|
|
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<motion.div
|
|
variants={dropdownVariants}
|
|
initial="hidden"
|
|
animate="visible"
|
|
exit="exit"
|
|
className="absolute top-full right-0 mt-1 bg-white rounded-lg shadow-lg border border-gray-200 py-1 min-w-[140px] z-50"
|
|
>
|
|
{Object.entries(languages).map(([code, lang]) => (
|
|
<button
|
|
key={code}
|
|
onClick={() => handleLanguageChange(code)}
|
|
className={`w-full px-3 py-2 text-left hover:bg-gray-50 transition-colors flex items-center gap-2 ${
|
|
code === currentLang ? 'bg-purple-50 text-purple-600' : 'text-gray-700'
|
|
}`}
|
|
>
|
|
<span className="text-base">{lang.flag}</span>
|
|
<span className="text-sm">{lang.name}</span>
|
|
</button>
|
|
))}
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (variant === 'mobile') {
|
|
return (
|
|
<div className={`w-full ${className}`}>
|
|
<button
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className="w-full flex items-center justify-between p-3 bg-white rounded-lg border border-gray-200 hover:border-purple-300 transition-colors"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<Globe className="w-5 h-5 text-gray-500" />
|
|
<div className="text-left">
|
|
<div className="text-sm font-medium text-gray-900">
|
|
Language / Idioma
|
|
</div>
|
|
<div className="text-xs text-gray-500">
|
|
{currentLanguage?.flag} {currentLanguage?.name}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<ChevronDown className={`w-4 h-4 text-gray-500 transition-transform ${
|
|
isOpen ? 'rotate-180' : ''
|
|
}`} />
|
|
</button>
|
|
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<motion.div
|
|
variants={dropdownVariants}
|
|
initial="hidden"
|
|
animate="visible"
|
|
exit="exit"
|
|
className="mt-2 bg-white rounded-lg shadow-lg border border-gray-200 overflow-hidden"
|
|
>
|
|
{Object.entries(languages).map(([code, lang]) => (
|
|
<button
|
|
key={code}
|
|
onClick={() => handleLanguageChange(code)}
|
|
className={`w-full px-4 py-3 text-left hover:bg-gray-50 transition-colors border-b border-gray-100 last:border-b-0 ${
|
|
code === currentLang ? 'bg-purple-50 text-purple-600' : 'text-gray-700'
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-xl">{lang.flag}</span>
|
|
<div>
|
|
<div className="font-medium">{lang.name}</div>
|
|
<div className="text-sm text-gray-500">{code.toUpperCase()}</div>
|
|
</div>
|
|
{code === currentLang && (
|
|
<div className="ml-auto w-2 h-2 bg-purple-600 rounded-full" />
|
|
)}
|
|
</div>
|
|
</button>
|
|
))}
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Default variant
|
|
return (
|
|
<div className={`relative ${className}`}>
|
|
<button
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className="flex items-center gap-2 px-3 py-2 bg-white rounded-lg border border-gray-200 hover:border-purple-300 transition-colors shadow-sm"
|
|
aria-label={t('accessibility.toggleLanguage')}
|
|
>
|
|
<Globe className="w-4 h-4 text-gray-500" />
|
|
<span className="text-lg">{currentLanguage?.flag || '🌐'}</span>
|
|
<span className="text-sm font-medium text-gray-700">
|
|
{currentLanguage?.name || 'English'}
|
|
</span>
|
|
<ChevronDown className={`w-4 h-4 text-gray-500 transition-transform ${
|
|
isOpen ? 'rotate-180' : ''
|
|
}`} />
|
|
</button>
|
|
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<>
|
|
{/* Backdrop */}
|
|
<div
|
|
className="fixed inset-0 z-40"
|
|
onClick={() => setIsOpen(false)}
|
|
/>
|
|
|
|
{/* Dropdown */}
|
|
<motion.div
|
|
variants={dropdownVariants}
|
|
initial="hidden"
|
|
animate="visible"
|
|
exit="exit"
|
|
className="absolute top-full right-0 mt-2 bg-white rounded-lg shadow-xl border border-gray-200 py-2 min-w-[200px] z-50"
|
|
>
|
|
<div className="px-3 py-2 border-b border-gray-100">
|
|
<div className="text-xs font-medium text-gray-500 uppercase tracking-wide">
|
|
Select Language
|
|
</div>
|
|
</div>
|
|
|
|
{Object.entries(languages).map(([code, lang]) => (
|
|
<button
|
|
key={code}
|
|
onClick={() => handleLanguageChange(code)}
|
|
className={`w-full px-4 py-2 text-left hover:bg-gray-50 transition-colors flex items-center gap-3 ${
|
|
code === currentLang ? 'bg-purple-50 text-purple-600' : 'text-gray-700'
|
|
}`}
|
|
>
|
|
<span className="text-xl">{lang.flag}</span>
|
|
<div className="flex-1">
|
|
<div className="font-medium">{lang.name}</div>
|
|
<div className="text-xs text-gray-500">{code.toUpperCase()}</div>
|
|
</div>
|
|
{code === currentLang && (
|
|
<div className="w-2 h-2 bg-purple-600 rounded-full" />
|
|
)}
|
|
</button>
|
|
))}
|
|
</motion.div>
|
|
</>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default LanguageSwitcher |