feat: add unified mailbox across accounts and sidebar icons toggle
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
import { type SVGProps, type ReactElement } from "react";
|
||||
|
||||
type FlagProps = SVGProps<SVGSVGElement>;
|
||||
|
||||
const flagClass = "inline-block rounded-[2px] shrink-0";
|
||||
const W = 20;
|
||||
const H = 15;
|
||||
|
||||
/** Great Britain – Union Jack (simplified) */
|
||||
export function FlagGB(props: FlagProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 30" width={W} height={H} className={flagClass} {...props}>
|
||||
<rect width="60" height="30" fill="#012169" />
|
||||
<path d="M0,0 L60,30 M60,0 L0,30" stroke="#fff" strokeWidth="6" />
|
||||
<path d="M0,0 L60,30 M60,0 L0,30" stroke="#C8102E" strokeWidth="2" />
|
||||
<path d="M30,0 V30 M0,15 H60" stroke="#fff" strokeWidth="10" />
|
||||
<path d="M30,0 V30 M0,15 H60" stroke="#C8102E" strokeWidth="6" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** France – Blue, White, Red vertical */
|
||||
export function FlagFR(props: FlagProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2" width={W} height={H} className={flagClass} {...props}>
|
||||
<rect width="1" height="2" fill="#002395" />
|
||||
<rect x="1" width="1" height="2" fill="#fff" />
|
||||
<rect x="2" width="1" height="2" fill="#ED2939" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** Japan – White with red circle */
|
||||
export function FlagJP(props: FlagProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2" width={W} height={H} className={flagClass} {...props}>
|
||||
<rect width="3" height="2" fill="#fff" />
|
||||
<circle cx="1.5" cy="1" r="0.6" fill="#BC002D" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** South Korea – Simplified */
|
||||
export function FlagKR(props: FlagProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2" width={W} height={H} className={flagClass} {...props}>
|
||||
<rect width="3" height="2" fill="#fff" />
|
||||
<circle cx="1.5" cy="1" r="0.55" fill="#CD2E3A" />
|
||||
<path d="M1.5,1 a0.275,0.275 0 0,1 0,0.55 a0.275,0.275 0 0,0 0,-0.55" fill="#0047A0" />
|
||||
<path d="M1.5,1 a0.275,0.275 0 0,0 0,-0.55 a0.275,0.275 0 0,1 0,0.55" fill="#0047A0" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** Spain – Red, Yellow, Red horizontal */
|
||||
export function FlagES(props: FlagProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2" width={W} height={H} className={flagClass} {...props}>
|
||||
<rect width="3" height="0.5" fill="#AA151B" />
|
||||
<rect y="0.5" width="3" height="1" fill="#F1BF00" />
|
||||
<rect y="1.5" width="3" height="0.5" fill="#AA151B" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** Italy – Green, White, Red vertical */
|
||||
export function FlagIT(props: FlagProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2" width={W} height={H} className={flagClass} {...props}>
|
||||
<rect width="1" height="2" fill="#009246" />
|
||||
<rect x="1" width="1" height="2" fill="#fff" />
|
||||
<rect x="2" width="1" height="2" fill="#CE2B37" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** Germany – Black, Red, Gold horizontal */
|
||||
export function FlagDE(props: FlagProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5 3" width={W} height={H} className={flagClass} {...props}>
|
||||
<rect width="5" height="1" fill="#000" />
|
||||
<rect y="1" width="5" height="1" fill="#DD0000" />
|
||||
<rect y="2" width="5" height="1" fill="#FFCC00" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** Latvia – Maroon, White, Maroon horizontal */
|
||||
export function FlagLV(props: FlagProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 10" width={W} height={H} className={flagClass} {...props}>
|
||||
<rect width="20" height="4" fill="#9E3039" />
|
||||
<rect y="4" width="20" height="2" fill="#fff" />
|
||||
<rect y="6" width="20" height="4" fill="#9E3039" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** Netherlands – Red, White, Blue horizontal */
|
||||
export function FlagNL(props: FlagProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 9 6" width={W} height={H} className={flagClass} {...props}>
|
||||
<rect width="9" height="2" fill="#AE1C28" />
|
||||
<rect y="2" width="9" height="2" fill="#fff" />
|
||||
<rect y="4" width="9" height="2" fill="#21468B" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** Poland – White, Red horizontal */
|
||||
export function FlagPL(props: FlagProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 5" width={W} height={H} className={flagClass} {...props}>
|
||||
<rect width="8" height="2.5" fill="#fff" />
|
||||
<rect y="2.5" width="8" height="2.5" fill="#DC143C" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** Brazil – Green, yellow diamond (simplified) */
|
||||
export function FlagBR(props: FlagProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 14" width={W} height={H} className={flagClass} {...props}>
|
||||
<rect width="20" height="14" fill="#009B3A" />
|
||||
<polygon points="10,1.5 18.5,7 10,12.5 1.5,7" fill="#FEDF00" />
|
||||
<circle cx="10" cy="7" r="3" fill="#002776" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** Russia – White, Blue, Red horizontal */
|
||||
export function FlagRU(props: FlagProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 9 6" width={W} height={H} className={flagClass} {...props}>
|
||||
<rect width="9" height="2" fill="#fff" />
|
||||
<rect y="2" width="9" height="2" fill="#0039A6" />
|
||||
<rect y="4" width="9" height="2" fill="#D52B1E" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** Ukraine – Blue, Yellow horizontal */
|
||||
export function FlagUA(props: FlagProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2" width={W} height={H} className={flagClass} {...props}>
|
||||
<rect width="3" height="1" fill="#005BBB" />
|
||||
<rect y="1" width="3" height="1" fill="#FFD500" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** China – Red with yellow stars (simplified) */
|
||||
export function FlagCN(props: FlagProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 20" width={W} height={H} className={flagClass} {...props}>
|
||||
<rect width="30" height="20" fill="#DE2910" />
|
||||
<g fill="#FFDE00">
|
||||
<polygon points="5,2 6,5 3.2,3.2 6.8,3.2 4,5" />
|
||||
<polygon points="10,1 10.6,2.7 9,1.8 11,1.8 9.4,2.7" />
|
||||
<polygon points="12,3 12.6,4.7 11,3.8 13,3.8 11.4,4.7" />
|
||||
<polygon points="12,6 12.6,7.7 11,6.8 13,6.8 11.4,7.7" />
|
||||
<polygon points="10,8 10.6,9.7 9,8.8 11,8.8 9.4,9.7" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** Map locale codes to flag components */
|
||||
export const flagComponents: Record<string, (props: FlagProps) => ReactElement> = {
|
||||
en: FlagGB,
|
||||
fr: FlagFR,
|
||||
ja: FlagJP,
|
||||
ko: FlagKR,
|
||||
es: FlagES,
|
||||
it: FlagIT,
|
||||
de: FlagDE,
|
||||
lv: FlagLV,
|
||||
nl: FlagNL,
|
||||
pl: FlagPL,
|
||||
pt: FlagBR,
|
||||
ru: FlagRU,
|
||||
uk: FlagUA,
|
||||
zh: FlagCN,
|
||||
};
|
||||
@@ -1,37 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useLocale } from 'next-intl';
|
||||
import { useLocaleStore } from '@/stores/locale-store';
|
||||
import { Select } from '@/components/settings/settings-section';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { flagComponents } from './flag-icons';
|
||||
|
||||
const languages = [
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'fr', label: 'Français' },
|
||||
{ value: 'ja', label: '日本語' },
|
||||
{ value: 'ko', label: '한국어' },
|
||||
{ value: 'es', label: 'Español' },
|
||||
{ value: 'it', label: 'Italiano' },
|
||||
{ value: 'de', label: 'Deutsch' },
|
||||
{ value: 'lv', label: 'Latviešu' },
|
||||
{ value: 'nl', label: 'Nederlands' },
|
||||
{ value: 'pl', label: 'Polski' },
|
||||
{ value: 'pt', label: 'Português' },
|
||||
{ value: 'ru', label: 'Русский' },
|
||||
{ value: 'uk', label: 'Українська' },
|
||||
{ value: 'zh', label: '简体中文' },
|
||||
];
|
||||
|
||||
function FlagIcon({ locale }: { locale: string }) {
|
||||
const Flag = flagComponents[locale];
|
||||
if (!Flag) return null;
|
||||
return <Flag />;
|
||||
}
|
||||
|
||||
export function LanguageSwitcher({ className }: { className?: string }) {
|
||||
const currentLocale = useLocale();
|
||||
const setLocale = useLocaleStore((state) => state.setLocale);
|
||||
const [open, setOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const listRef = useRef<HTMLUListElement>(null);
|
||||
|
||||
const languages = [
|
||||
{ value: 'en', label: '🇬🇧 English' },
|
||||
{ value: 'fr', label: '🇫🇷 Français' },
|
||||
{ value: 'ja', label: '🇯🇵 日本語' },
|
||||
{ value: 'ko', label: '🇰🇷 한국어' },
|
||||
{ value: 'es', label: '🇪🇸 Español' },
|
||||
{ value: 'it', label: '🇮🇹 Italiano' },
|
||||
{ value: 'de', label: '🇩🇪 Deutsch' },
|
||||
{ value: 'lv', label: '🇱🇻 Latviešu' },
|
||||
{ value: 'nl', label: '🇳🇱 Nederlands' },
|
||||
{ value: 'pl', label: '🇵🇱 Polski' },
|
||||
{ value: 'pt', label: '🇧🇷 Português' },
|
||||
{ value: 'ru', label: '🇷🇺 Русский' },
|
||||
{ value: 'uk', label: '🇺🇦 Українська' },
|
||||
{ value: 'zh', label: '🇨🇳 简体中文' }
|
||||
];
|
||||
const current = languages.find((l) => l.value === currentLocale) ?? languages[0];
|
||||
|
||||
// Close on outside click
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
function handleClick(e: MouseEvent) {
|
||||
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
return () => document.removeEventListener("mousedown", handleClick);
|
||||
}, [open]);
|
||||
|
||||
// Close on Escape
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
function handleKey(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") setOpen(false);
|
||||
}
|
||||
document.addEventListener("keydown", handleKey);
|
||||
return () => document.removeEventListener("keydown", handleKey);
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Select
|
||||
value={currentLocale}
|
||||
onChange={setLocale}
|
||||
options={languages}
|
||||
/>
|
||||
<div ref={containerRef} className={cn("relative", className)}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="flex items-center gap-2 px-3 py-1.5 text-sm rounded-md bg-muted border border-border text-foreground hover:border-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring transition-colors duration-150 cursor-pointer w-full"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={open}
|
||||
>
|
||||
<FlagIcon locale={current.value} />
|
||||
<span className="flex-1 text-left">{current.label}</span>
|
||||
<ChevronDown className={cn("h-3.5 w-3.5 text-muted-foreground transition-transform duration-150", open && "rotate-180")} />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<ul
|
||||
ref={listRef}
|
||||
role="listbox"
|
||||
aria-activedescendant={`lang-${currentLocale}`}
|
||||
className="absolute z-50 mt-1 w-full max-h-60 overflow-auto rounded-md border border-border bg-background shadow-lg py-1"
|
||||
>
|
||||
{languages.map((lang) => (
|
||||
<li
|
||||
key={lang.value}
|
||||
id={`lang-${lang.value}`}
|
||||
role="option"
|
||||
aria-selected={lang.value === currentLocale}
|
||||
onClick={() => {
|
||||
setLocale(lang.value);
|
||||
setOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-1.5 text-sm cursor-pointer transition-colors duration-100",
|
||||
lang.value === currentLocale
|
||||
? "bg-accent text-accent-foreground font-medium"
|
||||
: "text-foreground hover:bg-accent/50"
|
||||
)}
|
||||
>
|
||||
<FlagIcon locale={lang.value} />
|
||||
<span>{lang.label}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user