fix(menus): move language into its own mobile view; fit crew tabs
This commit is contained in:
parent
5ac9e67860
commit
74249e304e
5 changed files with 154 additions and 83 deletions
|
|
@ -7,47 +7,10 @@ import {
|
|||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useTranslation, type Language } from '@/contexts/language';
|
||||
import { useTranslation } from '@/contexts/language';
|
||||
import { LANGUAGE_OPTIONS } from '@/lib/languageOptions';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface LanguageOption {
|
||||
code: Language;
|
||||
name: string;
|
||||
nativeName: string;
|
||||
flag: string;
|
||||
}
|
||||
|
||||
const languages: LanguageOption[] = [
|
||||
{ code: 'en', name: 'English', nativeName: 'English', flag: '/flags/gb-eng.svg' },
|
||||
{ code: 'es', name: 'Spanish', nativeName: 'Español', flag: '🇪🇸' },
|
||||
{ code: 'ca', name: 'Catalan', nativeName: 'Català', flag: '/flags/ca.svg' },
|
||||
{ code: 'eu', name: 'Basque', nativeName: 'Euskara', flag: '/flags/eu.svg' },
|
||||
{ code: 'gl', name: 'Galician', nativeName: 'Galego', flag: '/flags/gl.svg' },
|
||||
{ code: 'pt', name: 'Portuguese (Brazil)', nativeName: 'Português (Brasil)', flag: '🇧🇷' },
|
||||
{ code: 'pt-PT', name: 'Portuguese (Portugal)', nativeName: 'Português (Portugal)', flag: '🇵🇹' },
|
||||
{ code: 'fr', name: 'French', nativeName: 'Français', flag: '🇫🇷' },
|
||||
{ code: 'de', name: 'German', nativeName: 'Deutsch', flag: '🇩🇪' },
|
||||
{ code: 'it', name: 'Italian', nativeName: 'Italiano', flag: '🇮🇹' },
|
||||
{ code: 'nl', name: 'Dutch', nativeName: 'Nederlands', flag: '🇳🇱' },
|
||||
{ code: 'pl', name: 'Polish', nativeName: 'Polski', flag: '🇵🇱' },
|
||||
{ code: 'ru', name: 'Russian', nativeName: 'Русский', flag: '🇷🇺' },
|
||||
{ code: 'ko', name: 'Korean', nativeName: '한국어', flag: '🇰🇷' },
|
||||
{ code: 'ko-KP', name: 'Korean (North)', nativeName: '조선어', flag: '🇰🇵' },
|
||||
{ code: 'th', name: 'Thai', nativeName: 'ไทย', flag: '🇹🇭' },
|
||||
{ code: 'ja', name: 'Japanese', nativeName: '日本語', flag: '🇯🇵' },
|
||||
{ code: 'zh-CN', name: 'Chinese (Simplified)', nativeName: '简体中文', flag: '🇨🇳' },
|
||||
{ code: 'zh-TW', name: 'Chinese (Traditional)', nativeName: '繁體中文', flag: '🇹🇼' },
|
||||
{ code: 'uk', name: 'Ukrainian', nativeName: 'Українська', flag: '🇺🇦' },
|
||||
{ code: 'id', name: 'Indonesian', nativeName: 'Bahasa Indonesia', flag: '🇮🇩' },
|
||||
{ code: 'cs', name: 'Czech', nativeName: 'Čeština', flag: '🇨🇿' },
|
||||
{ code: 'ro', name: 'Romanian', nativeName: 'Română', flag: '🇷🇴' },
|
||||
{ code: 'sv', name: 'Swedish', nativeName: 'Svenska', flag: '🇸🇪' },
|
||||
{ code: 'el', name: 'Greek', nativeName: 'Ελληνικά', flag: '🇬🇷' },
|
||||
{ code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt', flag: '🇻🇳' },
|
||||
{ code: 'tr', name: 'Turkish', nativeName: 'Türkçe', flag: '🇹🇷' },
|
||||
{ code: 'hi', name: 'Hindi', nativeName: 'हिन्दी', flag: '🇮🇳' },
|
||||
];
|
||||
|
||||
interface LanguageToggleProps {
|
||||
/** Show only the flag icon (compact mode) */
|
||||
compact?: boolean;
|
||||
|
|
@ -93,17 +56,23 @@ export function LanguageToggle({ compact = false, inline = false, className, var
|
|||
const { language, setLanguage, t } = useTranslation();
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
// Auto-focusing the search on a phone pops the keyboard over the list before
|
||||
// the user can see the languages. Keep it for mouse/pen, not touch.
|
||||
const autoFocusSearch =
|
||||
typeof window !== 'undefined' && !!window.matchMedia?.('(pointer: fine)').matches;
|
||||
|
||||
const safeQuery = query.trim().toLowerCase();
|
||||
const filteredLanguages = safeQuery
|
||||
? languages.filter(
|
||||
? LANGUAGE_OPTIONS.filter(
|
||||
(l) =>
|
||||
l.name.toLowerCase().includes(safeQuery) ||
|
||||
l.nativeName.toLowerCase().includes(safeQuery) ||
|
||||
l.code.toLowerCase().includes(safeQuery)
|
||||
)
|
||||
: languages;
|
||||
: LANGUAGE_OPTIONS;
|
||||
|
||||
const currentLanguage = languages.find(l => l.code === language) || languages[0];
|
||||
const currentLanguage =
|
||||
LANGUAGE_OPTIONS.find((l) => l.code === language) || LANGUAGE_OPTIONS[0];
|
||||
|
||||
if (inline) {
|
||||
return (
|
||||
|
|
@ -177,7 +146,7 @@ export function LanguageToggle({ compact = false, inline = false, className, var
|
|||
onPointerDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<input
|
||||
autoFocus
|
||||
autoFocus={autoFocusSearch}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder={t('nav.searchLanguages')}
|
||||
|
|
@ -189,7 +158,7 @@ export function LanguageToggle({ compact = false, inline = false, className, var
|
|||
key={lang.code}
|
||||
onClick={() => setLanguage(lang.code)}
|
||||
className={cn(
|
||||
'flex items-center gap-3 cursor-pointer',
|
||||
'flex min-h-11 items-center gap-3 cursor-pointer',
|
||||
language === lang.code && 'bg-primary/10 text-primary'
|
||||
)}
|
||||
>
|
||||
|
|
@ -225,7 +194,7 @@ export function LanguageSelector() {
|
|||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">{t('settings.selectLanguage')}</label>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{languages.map((lang) => (
|
||||
{LANGUAGE_OPTIONS.map((lang) => (
|
||||
<button
|
||||
key={lang.code}
|
||||
onClick={() => setLanguage(lang.code)}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
import { LoginArea } from '@/components/auth/LoginArea';
|
||||
import { useSasquatchGuide } from '@/hooks/useSasquatchGuide';
|
||||
import { LanguageToggle } from '@/components/LanguageToggle';
|
||||
import { LANGUAGE_OPTIONS } from '@/lib/languageOptions';
|
||||
import { clearTranslationMemory } from '@/lib/translation';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useTranslation } from '@/contexts/language';
|
||||
|
|
@ -46,6 +47,9 @@ import {
|
|||
SlidersHorizontal,
|
||||
BookOpen,
|
||||
PersonStanding,
|
||||
Languages,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
} from 'lucide-react';
|
||||
import { OfflineIndicator } from '@/components/OfflineIndicator';
|
||||
import { UserStatusIndicator } from '@/components/UserStatusIndicator';
|
||||
|
|
@ -150,14 +154,14 @@ function NavLink({
|
|||
to={href}
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200',
|
||||
'flex min-w-0 items-center gap-3 px-4 py-3 rounded-xl text-left transition-all duration-200',
|
||||
isActive
|
||||
? 'bg-primary text-primary-foreground shadow-lg shadow-primary/25'
|
||||
: 'hover:bg-muted text-muted-foreground hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
<span className="font-medium">{label}</span>
|
||||
<Icon className="h-5 w-5 shrink-0" />
|
||||
<span className="min-w-0 flex-1 break-words font-medium">{label}</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
|
@ -203,7 +207,7 @@ function MobileHeaderUserMenu({
|
|||
<span className="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 bg-green-500 border-2 border-black rounded-full xs:hidden" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56 p-2">
|
||||
<DropdownMenuContent align="end" className="w-56 p-2 [&_[role=menuitem]]:min-h-11">
|
||||
{/* User info header */}
|
||||
<div className="px-2 py-3 mb-2 rounded-lg bg-green-500/10 border border-green-500/20">
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -513,6 +517,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
|||
// padding even though it shares this one persistent layout with every route.
|
||||
const isFullBleed = noPadding || location.pathname === '/' || location.pathname === '/map';
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [mobileLanguageView, setMobileLanguageView] = useState(false);
|
||||
const [showStatusDialog, setShowStatusDialog] = useState(false);
|
||||
const [showCustomizeNav, setShowCustomizeNav] = useState(false);
|
||||
const [showAddAccount, setShowAddAccount] = useState(false);
|
||||
|
|
@ -539,7 +544,8 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
|||
const timer = window.setTimeout(() => setShowWelcomeLogin(true), 900);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [user]);
|
||||
const { t } = useTranslation();
|
||||
const { t, language } = useTranslation();
|
||||
const currentLanguage = LANGUAGE_OPTIONS.find((item) => item.code === language) ?? LANGUAGE_OPTIONS[0];
|
||||
const { currentUser: currentAccount, removeLogin } = useLoggedInAccounts();
|
||||
const author = useAuthor(user?.pubkey || '');
|
||||
const displayName = author.data?.metadata?.name ?? genUserName(user?.pubkey);
|
||||
|
|
@ -624,7 +630,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
|||
{t('nav.more')}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-64">
|
||||
<DropdownMenuContent align="end" className="w-64 [&_[role=menuitem]]:min-h-10">
|
||||
{/* Catalog items the user hasn't pinned to the top bar.
|
||||
Auth-only items (stash/collections/contacts/wallet/messages)
|
||||
only show when logged in. */}
|
||||
|
|
@ -822,7 +828,13 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
|||
<LoginArea compact className="flex flex-row gap-1" />
|
||||
</div>
|
||||
|
||||
<Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>
|
||||
<Sheet
|
||||
open={mobileMenuOpen}
|
||||
onOpenChange={(open) => {
|
||||
setMobileMenuOpen(open);
|
||||
if (!open) setMobileLanguageView(false);
|
||||
}}
|
||||
>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size="icon" aria-label={t('chromeUi.menu')} className="h-10 w-10 touch-manipulation active:scale-95 transition-transform">
|
||||
<Menu className="h-5 w-5" />
|
||||
|
|
@ -830,7 +842,43 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
|||
</SheetTrigger>
|
||||
<SheetContent side="right" className="w-80 overflow-y-auto">
|
||||
<SheetTitle className="sr-only">{t('chromeUi.menu')}</SheetTitle>
|
||||
{mobileLanguageView ? (
|
||||
<div className="flex flex-col gap-4 mt-6 pb-8">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMobileLanguageView(false)}
|
||||
className="flex items-center gap-2 self-start rounded-lg px-1 py-1 text-sm font-bold text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" /> {t('common.back')}
|
||||
</button>
|
||||
<div>
|
||||
<h2 className="text-xl font-black uppercase tracking-wide">
|
||||
{t('settings.selectLanguage')}
|
||||
</h2>
|
||||
<LanguageToggle inline className="mt-3 w-full" variant="ghost" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-6 mt-6 pb-8">
|
||||
{/* Language picker lives in its own view — 28 languages inline
|
||||
made this sheet ~3,200px tall and buried the control. */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMobileLanguageView(true)}
|
||||
className="flex w-full items-center gap-3 rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-left transition-colors hover:bg-white/10 active:scale-[0.99]"
|
||||
>
|
||||
<Languages className="h-5 w-5 shrink-0 text-teal-400" />
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block text-[10px] font-black uppercase tracking-widest text-muted-foreground">
|
||||
{t('settings.selectLanguage')}
|
||||
</span>
|
||||
<span className="block truncate text-sm font-medium">
|
||||
{currentLanguage.nativeName}
|
||||
</span>
|
||||
</span>
|
||||
<ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
</button>
|
||||
|
||||
{/* Wallet Card - Mobile Menu */}
|
||||
{user && (
|
||||
<Link
|
||||
|
|
@ -1005,11 +1053,6 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
|||
<span className="font-medium">{t('chromeUi.customizeNavBar')}</span>
|
||||
</button>
|
||||
</div>
|
||||
{/* Language Toggle in Mobile Menu — inline (a portaled dropdown
|
||||
is unclickable inside a Radix Sheet) */}
|
||||
<div className="py-3 border-t">
|
||||
<LanguageToggle inline className="w-full" variant="ghost" />
|
||||
</div>
|
||||
{/* Login/Logout Section */}
|
||||
<div className="pt-4 border-t space-y-3">
|
||||
{user ? (
|
||||
|
|
@ -1063,6 +1106,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
Check,
|
||||
ChevronDown,
|
||||
Copy,
|
||||
ExternalLink,
|
||||
ImagePlus,
|
||||
KeyRound,
|
||||
Loader2,
|
||||
|
|
@ -780,6 +781,7 @@ function AdvancedPanel({
|
|||
</span>
|
||||
</div>
|
||||
{groupRef && (
|
||||
<>
|
||||
<div className="flex items-center gap-2 text-[11px] text-gray-500">
|
||||
<span className="shrink-0">{t('cordnUi.groupRef')}</span>
|
||||
<code className="min-w-0 flex-1 truncate rounded bg-black/40 px-1.5 py-0.5 text-gray-400">
|
||||
|
|
@ -799,6 +801,16 @@ function AdvancedPanel({
|
|||
{copied ? t('cordnUi.copied') : t('cordnUi.copyRef')}
|
||||
</Button>
|
||||
</div>
|
||||
<a
|
||||
href={`https://cordn.net/chat/${groupRef}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-[11px] font-medium text-teal-300 transition-colors hover:text-teal-200"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />{' '}
|
||||
{t('socialUi.crewCoordinator.openCordnGroup')}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
|
|
|
|||
43
src/lib/languageOptions.ts
Normal file
43
src/lib/languageOptions.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import type { Language } from '@/contexts/language';
|
||||
|
||||
export interface LanguageOption {
|
||||
code: Language;
|
||||
name: string;
|
||||
nativeName: string;
|
||||
flag: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every locale the picker can show. Kept out of the component file so Fast
|
||||
* Refresh still works (a component module must only export components).
|
||||
*/
|
||||
export const LANGUAGE_OPTIONS: LanguageOption[] = [
|
||||
{ code: 'en', name: 'English', nativeName: 'English', flag: '/flags/gb-eng.svg' },
|
||||
{ code: 'es', name: 'Spanish', nativeName: 'Español', flag: '🇪🇸' },
|
||||
{ code: 'ca', name: 'Catalan', nativeName: 'Català', flag: '/flags/ca.svg' },
|
||||
{ code: 'eu', name: 'Basque', nativeName: 'Euskara', flag: '/flags/eu.svg' },
|
||||
{ code: 'gl', name: 'Galician', nativeName: 'Galego', flag: '/flags/gl.svg' },
|
||||
{ code: 'pt', name: 'Portuguese (Brazil)', nativeName: 'Português (Brasil)', flag: '🇧🇷' },
|
||||
{ code: 'pt-PT', name: 'Portuguese (Portugal)', nativeName: 'Português (Portugal)', flag: '🇵🇹' },
|
||||
{ code: 'fr', name: 'French', nativeName: 'Français', flag: '🇫🇷' },
|
||||
{ code: 'de', name: 'German', nativeName: 'Deutsch', flag: '🇩🇪' },
|
||||
{ code: 'it', name: 'Italian', nativeName: 'Italiano', flag: '🇮🇹' },
|
||||
{ code: 'nl', name: 'Dutch', nativeName: 'Nederlands', flag: '🇳🇱' },
|
||||
{ code: 'pl', name: 'Polish', nativeName: 'Polski', flag: '🇵🇱' },
|
||||
{ code: 'ru', name: 'Russian', nativeName: 'Русский', flag: '🇷🇺' },
|
||||
{ code: 'ko', name: 'Korean', nativeName: '한국어', flag: '🇰🇷' },
|
||||
{ code: 'ko-KP', name: 'Korean (North)', nativeName: '조선어', flag: '🇰🇵' },
|
||||
{ code: 'th', name: 'Thai', nativeName: 'ไทย', flag: '🇹🇭' },
|
||||
{ code: 'ja', name: 'Japanese', nativeName: '日本語', flag: '🇯🇵' },
|
||||
{ code: 'zh-CN', name: 'Chinese (Simplified)', nativeName: '简体中文', flag: '🇨🇳' },
|
||||
{ code: 'zh-TW', name: 'Chinese (Traditional)', nativeName: '繁體中文', flag: '🇹🇼' },
|
||||
{ code: 'uk', name: 'Ukrainian', nativeName: 'Українська', flag: '🇺🇦' },
|
||||
{ code: 'id', name: 'Indonesian', nativeName: 'Bahasa Indonesia', flag: '🇮🇩' },
|
||||
{ code: 'cs', name: 'Czech', nativeName: 'Čeština', flag: '🇨🇿' },
|
||||
{ code: 'ro', name: 'Romanian', nativeName: 'Română', flag: '🇷🇴' },
|
||||
{ code: 'sv', name: 'Swedish', nativeName: 'Svenska', flag: '🇸🇪' },
|
||||
{ code: 'el', name: 'Greek', nativeName: 'Ελληνικά', flag: '🇬🇷' },
|
||||
{ code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt', flag: '🇻🇳' },
|
||||
{ code: 'tr', name: 'Turkish', nativeName: 'Türkçe', flag: '🇹🇷' },
|
||||
{ code: 'hi', name: 'Hindi', nativeName: 'हिन्दी', flag: '🇮🇳' },
|
||||
];
|
||||
|
|
@ -347,18 +347,21 @@ export default function CrewDetailPage() {
|
|||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-3 bg-black/60 border border-white/10">
|
||||
<TabsTrigger value="events" className="gap-1.5 data-[state=active]:bg-[#ccff00]/20 data-[state=active]:text-[#ccff00]">
|
||||
<Calendar className="h-4 w-4" /> {t('socialUi.crew.eventsTab')}
|
||||
<TabsList className="grid w-full grid-cols-[1.1fr_1fr_1.3fr] bg-black/60 border border-white/10">
|
||||
<TabsTrigger value="events" className="min-w-0 gap-1.5 overflow-hidden text-[13px] data-[state=active]:bg-[#ccff00]/20 data-[state=active]:text-[#ccff00]">
|
||||
<Calendar className="h-4 w-4 shrink-0" />{' '}
|
||||
<span className="min-w-0 truncate">{t('socialUi.crew.eventsTab')}</span>
|
||||
{crewEvents && crewEvents.length > 0 && (
|
||||
<span className="ml-0.5 rounded-full bg-white/10 px-1.5 text-[10px]">{crewEvents.length}</span>
|
||||
<span className="ml-0.5 shrink-0 rounded-full bg-white/10 px-1.5 text-[10px]">{crewEvents.length}</span>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="chat" className="gap-1.5 data-[state=active]:bg-teal-500/20 data-[state=active]:text-teal-300">
|
||||
<MessageSquare className="h-4 w-4" /> {t('socialUi.crew.chatTab')}
|
||||
<TabsTrigger value="chat" className="min-w-0 gap-1.5 overflow-hidden text-[13px] data-[state=active]:bg-teal-500/20 data-[state=active]:text-teal-300">
|
||||
<MessageSquare className="h-4 w-4 shrink-0" />{' '}
|
||||
<span className="min-w-0 truncate">{t('socialUi.crew.chatTab')}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="mls" className="gap-1.5 data-[state=active]:bg-teal-500/20 data-[state=active]:text-teal-300">
|
||||
<Lock className="h-4 w-4" /> {t('cordnUi.title')}
|
||||
<TabsTrigger value="mls" className="min-w-0 gap-1.5 overflow-hidden text-[13px] data-[state=active]:bg-teal-500/20 data-[state=active]:text-teal-300">
|
||||
<Lock className="h-4 w-4 shrink-0" />{' '}
|
||||
<span className="min-w-0 truncate">{t('cordnUi.title')}</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue