fix(auth): open sign-in above menus and close the burger first

This commit is contained in:
SkateSpots 2026-10-27 20:48:04 +07:00
parent 7abeb2d030
commit 639afd8550
5 changed files with 94 additions and 34 deletions

View file

@ -22,6 +22,8 @@ interface LanguageToggleProps {
inline?: boolean;
/** Additional class names */
className?: string;
/** Called after a locale is chosen (e.g. to close a sheet sub-view). */
onSelect?: () => void;
/** Variant style */
variant?: 'default' | 'outline' | 'ghost';
}
@ -52,7 +54,7 @@ function Flag({ flag, className }: { flag: string; className?: string }) {
/**
* Language toggle dropdown for switching between the available locales
*/
export function LanguageToggle({ compact = false, inline = false, className, variant = 'ghost' }: LanguageToggleProps) {
export function LanguageToggle({ compact = false, inline = false, className, variant = 'ghost', onSelect }: LanguageToggleProps) {
const { language, setLanguage, t } = useTranslation();
const [query, setQuery] = useState('');
@ -89,7 +91,7 @@ export function LanguageToggle({ compact = false, inline = false, className, var
<button
key={lang.code}
type="button"
onClick={() => setLanguage(lang.code)}
onClick={() => { setLanguage(lang.code); onSelect?.(); }}
className={cn(
'flex items-center gap-3 rounded-xl px-4 py-3 text-left transition-all duration-200',
language === lang.code
@ -156,7 +158,7 @@ export function LanguageToggle({ compact = false, inline = false, className, var
{filteredLanguages.map((lang) => (
<DropdownMenuItem
key={lang.code}
onClick={() => setLanguage(lang.code)}
onClick={() => { setLanguage(lang.code); onSelect?.(); }}
className={cn(
'flex min-h-11 items-center gap-3 cursor-pointer',
language === lang.code && 'bg-primary/10 text-primary'

View file

@ -520,6 +520,9 @@ export function Layout({ children, noPadding }: LayoutProps) {
const isFullBleed = noPadding || location.pathname === '/' || location.pathname === '/map';
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [mobileLanguageView, setMobileLanguageView] = useState(false);
// One auth dialog for the header + burger menu. Owning it here (not inside
// LoginArea) keeps it above the sheet and outside the sheet's unmount tree.
const [authDialog, setAuthDialog] = useState<'login' | 'signup' | null>(null);
const [showStatusDialog, setShowStatusDialog] = useState(false);
const [showCustomizeNav, setShowCustomizeNav] = useState(false);
const [showAddAccount, setShowAddAccount] = useState(false);
@ -537,15 +540,28 @@ export function Layout({ children, noPadding }: LayoutProps) {
try { sessionStorage.setItem(WELCOME_LOGIN_SNOOZED, '1'); } catch { /* ignore */ }
setShowWelcomeLogin(false);
};
const requestAuth = (kind: 'login' | 'signup') => {
// Close every overlay that owns a lower z-index before opening the auth
// dialog. The burger sheet is z-2100; a dialog rendered inside it is z-50
// and becomes invisible/unclickable.
try { sessionStorage.setItem(WELCOME_LOGIN_SNOOZED, '1'); } catch { /* ignore */ }
setMobileMenuOpen(false);
setMobileLanguageView(false);
setShowWelcomeLogin(false);
setShowWelcomeSignup(false);
setAuthDialog(kind);
};
const requestLogin = () => requestAuth('login');
const requestSignup = () => requestAuth('signup');
useEffect(() => {
if (user || isNativePlatform()) return;
if (user || authDialog || isNativePlatform()) return;
try {
if (sessionStorage.getItem(WELCOME_LOGIN_SNOOZED) === '1') return;
} catch { return; }
if (typeof window === 'undefined' || !window.matchMedia?.('(min-width: 1024px)').matches) return;
const timer = window.setTimeout(() => setShowWelcomeLogin(true), 900);
return () => window.clearTimeout(timer);
}, [user]);
}, [user, authDialog]);
const { t, language } = useTranslation();
const currentLanguage = LANGUAGE_OPTIONS.find((item) => item.code === language) ?? LANGUAGE_OPTIONS[0];
const { currentUser: currentAccount, removeLogin } = useLoggedInAccounts();
@ -786,7 +802,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
<Settings className="h-5 w-5" />
</Button>
</Link>
<LoginArea className="max-w-60" />
<LoginArea className="max-w-60" onLoginRequest={requestLogin} onSignupRequest={requestSignup} />
</div>
</div>
</header>
@ -827,7 +843,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
/>
)}
<div className={cn(user && 'hidden')}>
<LoginArea compact className="flex flex-row gap-1" />
<LoginArea compact className="flex flex-row gap-1" onLoginRequest={requestLogin} onSignupRequest={requestSignup} />
</div>
<Sheet
@ -857,7 +873,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
<h2 className="text-xl font-black uppercase tracking-wide">
{t('settings.selectLanguage')}
</h2>
<LanguageToggle inline className="mt-3 w-full" variant="ghost" />
<LanguageToggle inline className="mt-3 w-full" variant="ghost" onSelect={() => setMobileLanguageView(false)} />
</div>
</div>
) : (
@ -1102,7 +1118,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
<p className="text-sm text-muted-foreground mb-4">
{t('chromeUi.loggedOutPrompt')}
</p>
<LoginArea className="w-full justify-center" />
<LoginArea className="w-full justify-center" onLoginRequest={requestLogin} onSignupRequest={requestSignup} />
</div>
</>
)}
@ -1256,6 +1272,24 @@ export function Layout({ children, noPadding }: LayoutProps) {
/>
</Suspense>
{/* Header/burger auth — owned here so closing the sheet cannot unmount it
and it always sits above the sheet + welcome overlay. */}
<Suspense fallback={null}>
<LazyLoginDialog
isOpen={authDialog === 'login'}
onClose={() => setAuthDialog(null)}
onLogin={() => setAuthDialog(null)}
onSignup={() => setAuthDialog('signup')}
/>
</Suspense>
<Suspense fallback={null}>
<LazySignupDialog
isOpen={authDialog === 'signup'}
onClose={() => setAuthDialog(null)}
onUseAmber={() => setAuthDialog('login')}
/>
</Suspense>
{/* Newsletter: floating pill + Monday admin reminder */}
<NewsletterOverlay />

View file

@ -15,14 +15,31 @@ export interface LoginAreaProps {
className?: string;
/** Compact mode for mobile headers - smaller buttons without text */
compact?: boolean;
/**
* Open auth in a caller-owned dialog instead of this component's local one.
* The mobile burger sheet uses this: its local dialog would portal *behind*
* the sheet (z-50 vs z-2100) and leave the menu looking stuck.
*/
onLoginRequest?: () => void;
onSignupRequest?: () => void;
}
export function LoginArea({ className, compact }: LoginAreaProps) {
export function LoginArea({ className, compact, onLoginRequest, onSignupRequest }: LoginAreaProps) {
const { currentUser } = useLoggedInAccounts();
const { t } = useTranslation();
const [loginDialogOpen, setLoginDialogOpen] = useState(false);
const [signupDialogOpen, setSignupDialogOpen] = useState(false);
const globalAuth = !!onLoginRequest && !!onSignupRequest;
const openLogin = () => {
if (onLoginRequest) onLoginRequest();
else setLoginDialogOpen(true);
};
const openSignup = () => {
if (onSignupRequest) onSignupRequest();
else setSignupDialogOpen(true);
};
const handleLogin = () => {
setLoginDialogOpen(false);
setSignupDialogOpen(false);
@ -31,12 +48,12 @@ export function LoginArea({ className, compact }: LoginAreaProps) {
return (
<div className={cn("inline-flex items-center justify-center", className)}>
{currentUser ? (
<AccountSwitcher onAddAccountClick={() => setLoginDialogOpen(true)} />
<AccountSwitcher onAddAccountClick={openLogin} />
) : compact ? (
// Compact mode for mobile headers - icon only
<div className="flex gap-1 justify-center">
<Button
onClick={() => setSignupDialogOpen(true)}
onClick={openSignup}
size="icon"
className='h-9 w-9 rounded-full bg-[#ccff00] text-black hover:bg-[#ccff00]/90'
title={t('settingsUi.loginArea.signUp')}
@ -44,7 +61,7 @@ export function LoginArea({ className, compact }: LoginAreaProps) {
<UserPlus className='w-4 h-4' />
</Button>
<Button
onClick={() => setLoginDialogOpen(true)}
onClick={openLogin}
variant="outline"
size="icon"
className="h-9 w-9 rounded-full border border-gray-600 hover:border-[#ff0040]"
@ -57,14 +74,14 @@ export function LoginArea({ className, compact }: LoginAreaProps) {
// Default full-size buttons
<div className="flex gap-3 justify-center w-full">
<Button
onClick={() => setSignupDialogOpen(true)}
onClick={openSignup}
className='flex items-center gap-2 px-5 py-2.5 rounded-full bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold transition-all flex-1 sm:flex-none'
>
<UserPlus className='w-4 h-4' />
<span>{t('settingsUi.loginArea.signUp')}</span>
</Button>
<Button
onClick={() => setLoginDialogOpen(true)}
onClick={openLogin}
variant="outline"
className="flex items-center gap-2 px-5 py-2.5 rounded-full font-medium transition-all border-2 border-gray-600 hover:border-[#ff0040] flex-1 sm:flex-none"
>
@ -74,24 +91,31 @@ export function LoginArea({ className, compact }: LoginAreaProps) {
</div>
)}
<LoginDialog
isOpen={loginDialogOpen}
onClose={() => setLoginDialogOpen(false)}
onLogin={handleLogin}
onSignup={() => setSignupDialogOpen(true)}
/>
{!globalAuth && (
<>
<LoginDialog
isOpen={loginDialogOpen}
onClose={() => setLoginDialogOpen(false)}
onLogin={handleLogin}
onSignup={() => {
setLoginDialogOpen(false);
setSignupDialogOpen(true);
}}
/>
{/* One signup entry point. The old "Recovery Phrase vs Secret Key" method
chooser was dead code (never opened) and pure jargon for a newcomer —
SignupDialog owns the whole flow now. */}
<SignupDialog
isOpen={signupDialogOpen}
onClose={() => setSignupDialogOpen(false)}
onUseAmber={() => {
setSignupDialogOpen(false);
setLoginDialogOpen(true);
}}
/>
{/* One signup entry point. The old "Recovery Phrase vs Secret Key"
method chooser was dead code (never opened) and pure jargon for a
newcomer — SignupDialog owns the whole flow now. */}
<SignupDialog
isOpen={signupDialogOpen}
onClose={() => setSignupDialogOpen(false)}
onUseAmber={() => {
setSignupDialogOpen(false);
setLoginDialogOpen(true);
}}
/>
</>
)}
</div>
);
}

View file

@ -321,7 +321,7 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className={cn("flex w-[calc(100%-1rem)] flex-col sm:max-w-md max-h-[calc(100dvh-2rem)] p-0 overflow-hidden rounded-2xl bg-black border-2 border-[#ff0040]/50")}>
<DialogContent className={cn("z-[2200] flex w-[calc(100%-1rem)] flex-col sm:max-w-md max-h-[calc(100dvh-2rem)] p-0 overflow-hidden rounded-2xl bg-black border-2 border-[#ff0040]/50")}>
<DialogHeader className={cn('px-5 pt-5 pb-3')}>
<DialogTitle className="text-center font-black uppercase tracking-wide text-lg">
{t('settingsUi.loginDialog.welcomeTitle')}

View file

@ -375,7 +375,7 @@ const SignupDialog: React.FC<SignupDialogProps> = ({ isOpen, onClose, onComplete
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent
className={cn("w-[calc(100%-1rem)] sm:max-w-md max-h-[90vh] max-h-[90dvh] p-0 overflow-hidden rounded-2xl flex flex-col")}
className={cn("z-[2200] w-[calc(100%-1rem)] sm:max-w-md max-h-[90vh] max-h-[90dvh] p-0 overflow-hidden rounded-2xl flex flex-col")}
>
<DialogHeader className={cn('px-4 sm:px-6 pt-4 sm:pt-6 pb-1 relative flex-shrink-0')}>
<DialogTitle className={cn('font-semibold text-center text-base sm:text-lg')}>