feat: redesign language switcher as dropdown, add cursor pointers
Replace flag-row selector with a dropdown to fix distorted flag icons and remove the colored ring border. Add cursor-pointer to the language menu and download submit button.
This commit is contained in:
@@ -1,44 +1,105 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useLocale } from 'next-intl'
|
import { useLocale } from 'next-intl'
|
||||||
import { useRouter, usePathname } from '@/navigation'
|
import { useRouter, usePathname } from '@/navigation'
|
||||||
import { routing } from '@/i18n/routing'
|
import { routing } from '@/i18n/routing'
|
||||||
|
|
||||||
const FLAG_LABELS: Record<string, string> = {
|
const LANGUAGE_LABELS: Record<string, string> = {
|
||||||
en: 'English',
|
en: 'English',
|
||||||
fr: 'Français',
|
fr: 'Français',
|
||||||
es: 'Español',
|
es: 'Español',
|
||||||
it: 'Italiano',
|
it: 'Italiano',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Flag({ locale, className }: { locale: string; className?: string }) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={`/flags/${locale}.svg`}
|
||||||
|
alt=""
|
||||||
|
className={['w-6 h-4 rounded-sm object-cover shrink-0', className].filter(Boolean).join(' ')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChevronIcon() {
|
||||||
|
return (
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
||||||
|
<polyline points="6 9 12 15 18 9" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function LanguageSwitcher() {
|
export function LanguageSwitcher() {
|
||||||
const locale = useLocale()
|
const locale = useLocale()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return
|
||||||
|
|
||||||
|
function handlePointerDown(event: MouseEvent) {
|
||||||
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Escape') setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handlePointerDown)
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handlePointerDown)
|
||||||
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
}
|
||||||
|
}, [open])
|
||||||
|
|
||||||
|
function selectLocale(loc: string) {
|
||||||
|
setOpen(false)
|
||||||
|
router.replace(pathname, { locale: loc })
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1">
|
<div ref={containerRef} className="relative">
|
||||||
{routing.locales.map((loc) => (
|
<button
|
||||||
<button
|
type="button"
|
||||||
key={loc}
|
onClick={() => setOpen((value) => !value)}
|
||||||
onClick={() => router.replace(pathname, { locale: loc })}
|
aria-haspopup="listbox"
|
||||||
aria-label={FLAG_LABELS[loc]}
|
aria-expanded={open}
|
||||||
className={[
|
className="flex items-center gap-2 rounded-lg py-1.5 pl-2 pr-2.5 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-800 transition-colors cursor-pointer"
|
||||||
'rounded transition-opacity focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500',
|
>
|
||||||
loc === locale
|
<Flag locale={locale} />
|
||||||
? 'opacity-100 ring-2 ring-violet-500 ring-offset-1 ring-offset-white dark:ring-offset-slate-950'
|
<ChevronIcon />
|
||||||
: 'opacity-40 hover:opacity-70',
|
</button>
|
||||||
].join(' ')}
|
|
||||||
|
{open && (
|
||||||
|
<ul
|
||||||
|
role="listbox"
|
||||||
|
className="absolute right-0 top-full mt-2 min-w-[9rem] rounded-lg border border-gray-200 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-lg py-1 z-50"
|
||||||
>
|
>
|
||||||
<img
|
{routing.locales.map((loc) => (
|
||||||
src={`/flags/${loc}.svg`}
|
<li key={loc} role="option" aria-selected={loc === locale}>
|
||||||
alt={FLAG_LABELS[loc]}
|
<button
|
||||||
width={28}
|
type="button"
|
||||||
height={20}
|
onClick={() => selectLocale(loc)}
|
||||||
className="rounded-sm block"
|
className={[
|
||||||
/>
|
'flex w-full items-center gap-2.5 px-3 py-2 text-sm text-left transition-colors cursor-pointer',
|
||||||
</button>
|
loc === locale
|
||||||
))}
|
? 'bg-violet-50 dark:bg-violet-500/10 text-violet-600 dark:text-violet-400 font-medium'
|
||||||
|
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-slate-800',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
<Flag locale={loc} />
|
||||||
|
{LANGUAGE_LABELS[loc]}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export function SubmitForm() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="flex items-center gap-2 px-5 py-3 bg-violet-600 hover:bg-violet-700 dark:bg-violet-500 dark:hover:bg-violet-600 text-white text-sm font-medium transition-colors disabled:opacity-60"
|
className="flex items-center gap-2 px-5 py-3 bg-violet-600 hover:bg-violet-700 dark:bg-violet-500 dark:hover:bg-violet-600 text-white text-sm font-medium transition-colors disabled:opacity-60 cursor-pointer disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{loading && <Spinner />}
|
{loading && <Spinner />}
|
||||||
{loading ? t('submitting') : t('submit')}
|
{loading ? t('submitting') : t('submit')}
|
||||||
|
|||||||
Reference in New Issue
Block a user