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; inline?: boolean;
/** Additional class names */ /** Additional class names */
className?: string; className?: string;
/** Called after a locale is chosen (e.g. to close a sheet sub-view). */
onSelect?: () => void;
/** Variant style */ /** Variant style */
variant?: 'default' | 'outline' | 'ghost'; 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 * 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 { language, setLanguage, t } = useTranslation();
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
@ -89,7 +91,7 @@ export function LanguageToggle({ compact = false, inline = false, className, var
<button <button
key={lang.code} key={lang.code}
type="button" type="button"
onClick={() => setLanguage(lang.code)} onClick={() => { setLanguage(lang.code); onSelect?.(); }}
className={cn( className={cn(
'flex items-center gap-3 rounded-xl px-4 py-3 text-left transition-all duration-200', 'flex items-center gap-3 rounded-xl px-4 py-3 text-left transition-all duration-200',
language === lang.code language === lang.code
@ -156,7 +158,7 @@ export function LanguageToggle({ compact = false, inline = false, className, var
{filteredLanguages.map((lang) => ( {filteredLanguages.map((lang) => (
<DropdownMenuItem <DropdownMenuItem
key={lang.code} key={lang.code}
onClick={() => setLanguage(lang.code)} onClick={() => { setLanguage(lang.code); onSelect?.(); }}
className={cn( className={cn(
'flex min-h-11 items-center gap-3 cursor-pointer', 'flex min-h-11 items-center gap-3 cursor-pointer',
language === lang.code && 'bg-primary/10 text-primary' 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 isFullBleed = noPadding || location.pathname === '/' || location.pathname === '/map';
const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [mobileLanguageView, setMobileLanguageView] = 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 [showStatusDialog, setShowStatusDialog] = useState(false);
const [showCustomizeNav, setShowCustomizeNav] = useState(false); const [showCustomizeNav, setShowCustomizeNav] = useState(false);
const [showAddAccount, setShowAddAccount] = 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 */ } try { sessionStorage.setItem(WELCOME_LOGIN_SNOOZED, '1'); } catch { /* ignore */ }
setShowWelcomeLogin(false); 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(() => { useEffect(() => {
if (user || isNativePlatform()) return; if (user || authDialog || isNativePlatform()) return;
try { try {
if (sessionStorage.getItem(WELCOME_LOGIN_SNOOZED) === '1') return; if (sessionStorage.getItem(WELCOME_LOGIN_SNOOZED) === '1') return;
} catch { return; } } catch { return; }
if (typeof window === 'undefined' || !window.matchMedia?.('(min-width: 1024px)').matches) return; if (typeof window === 'undefined' || !window.matchMedia?.('(min-width: 1024px)').matches) return;
const timer = window.setTimeout(() => setShowWelcomeLogin(true), 900); const timer = window.setTimeout(() => setShowWelcomeLogin(true), 900);
return () => window.clearTimeout(timer); return () => window.clearTimeout(timer);
}, [user]); }, [user, authDialog]);
const { t, language } = useTranslation(); const { t, language } = useTranslation();
const currentLanguage = LANGUAGE_OPTIONS.find((item) => item.code === language) ?? LANGUAGE_OPTIONS[0]; const currentLanguage = LANGUAGE_OPTIONS.find((item) => item.code === language) ?? LANGUAGE_OPTIONS[0];
const { currentUser: currentAccount, removeLogin } = useLoggedInAccounts(); const { currentUser: currentAccount, removeLogin } = useLoggedInAccounts();
@ -786,7 +802,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
<Settings className="h-5 w-5" /> <Settings className="h-5 w-5" />
</Button> </Button>
</Link> </Link>
<LoginArea className="max-w-60" /> <LoginArea className="max-w-60" onLoginRequest={requestLogin} onSignupRequest={requestSignup} />
</div> </div>
</div> </div>
</header> </header>
@ -827,7 +843,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
/> />
)} )}
<div className={cn(user && 'hidden')}> <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> </div>
<Sheet <Sheet
@ -857,7 +873,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
<h2 className="text-xl font-black uppercase tracking-wide"> <h2 className="text-xl font-black uppercase tracking-wide">
{t('settings.selectLanguage')} {t('settings.selectLanguage')}
</h2> </h2>
<LanguageToggle inline className="mt-3 w-full" variant="ghost" /> <LanguageToggle inline className="mt-3 w-full" variant="ghost" onSelect={() => setMobileLanguageView(false)} />
</div> </div>
</div> </div>
) : ( ) : (
@ -1102,7 +1118,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
<p className="text-sm text-muted-foreground mb-4"> <p className="text-sm text-muted-foreground mb-4">
{t('chromeUi.loggedOutPrompt')} {t('chromeUi.loggedOutPrompt')}
</p> </p>
<LoginArea className="w-full justify-center" /> <LoginArea className="w-full justify-center" onLoginRequest={requestLogin} onSignupRequest={requestSignup} />
</div> </div>
</> </>
)} )}
@ -1256,6 +1272,24 @@ export function Layout({ children, noPadding }: LayoutProps) {
/> />
</Suspense> </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 */} {/* Newsletter: floating pill + Monday admin reminder */}
<NewsletterOverlay /> <NewsletterOverlay />

View file

@ -15,14 +15,31 @@ export interface LoginAreaProps {
className?: string; className?: string;
/** Compact mode for mobile headers - smaller buttons without text */ /** Compact mode for mobile headers - smaller buttons without text */
compact?: boolean; 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 { currentUser } = useLoggedInAccounts();
const { t } = useTranslation(); const { t } = useTranslation();
const [loginDialogOpen, setLoginDialogOpen] = useState(false); const [loginDialogOpen, setLoginDialogOpen] = useState(false);
const [signupDialogOpen, setSignupDialogOpen] = 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 = () => { const handleLogin = () => {
setLoginDialogOpen(false); setLoginDialogOpen(false);
setSignupDialogOpen(false); setSignupDialogOpen(false);
@ -31,12 +48,12 @@ export function LoginArea({ className, compact }: LoginAreaProps) {
return ( return (
<div className={cn("inline-flex items-center justify-center", className)}> <div className={cn("inline-flex items-center justify-center", className)}>
{currentUser ? ( {currentUser ? (
<AccountSwitcher onAddAccountClick={() => setLoginDialogOpen(true)} /> <AccountSwitcher onAddAccountClick={openLogin} />
) : compact ? ( ) : compact ? (
// Compact mode for mobile headers - icon only // Compact mode for mobile headers - icon only
<div className="flex gap-1 justify-center"> <div className="flex gap-1 justify-center">
<Button <Button
onClick={() => setSignupDialogOpen(true)} onClick={openSignup}
size="icon" size="icon"
className='h-9 w-9 rounded-full bg-[#ccff00] text-black hover:bg-[#ccff00]/90' className='h-9 w-9 rounded-full bg-[#ccff00] text-black hover:bg-[#ccff00]/90'
title={t('settingsUi.loginArea.signUp')} title={t('settingsUi.loginArea.signUp')}
@ -44,7 +61,7 @@ export function LoginArea({ className, compact }: LoginAreaProps) {
<UserPlus className='w-4 h-4' /> <UserPlus className='w-4 h-4' />
</Button> </Button>
<Button <Button
onClick={() => setLoginDialogOpen(true)} onClick={openLogin}
variant="outline" variant="outline"
size="icon" size="icon"
className="h-9 w-9 rounded-full border border-gray-600 hover:border-[#ff0040]" 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 // Default full-size buttons
<div className="flex gap-3 justify-center w-full"> <div className="flex gap-3 justify-center w-full">
<Button <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' 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' /> <UserPlus className='w-4 h-4' />
<span>{t('settingsUi.loginArea.signUp')}</span> <span>{t('settingsUi.loginArea.signUp')}</span>
</Button> </Button>
<Button <Button
onClick={() => setLoginDialogOpen(true)} onClick={openLogin}
variant="outline" 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" 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> </div>
)} )}
<LoginDialog {!globalAuth && (
isOpen={loginDialogOpen} <>
onClose={() => setLoginDialogOpen(false)} <LoginDialog
onLogin={handleLogin} isOpen={loginDialogOpen}
onSignup={() => setSignupDialogOpen(true)} onClose={() => setLoginDialogOpen(false)}
/> onLogin={handleLogin}
onSignup={() => {
setLoginDialogOpen(false);
setSignupDialogOpen(true);
}}
/>
{/* One signup entry point. The old "Recovery Phrase vs Secret Key" method {/* One signup entry point. The old "Recovery Phrase vs Secret Key"
chooser was dead code (never opened) and pure jargon for a newcomer — method chooser was dead code (never opened) and pure jargon for a
SignupDialog owns the whole flow now. */} newcomer — SignupDialog owns the whole flow now. */}
<SignupDialog <SignupDialog
isOpen={signupDialogOpen} isOpen={signupDialogOpen}
onClose={() => setSignupDialogOpen(false)} onClose={() => setSignupDialogOpen(false)}
onUseAmber={() => { onUseAmber={() => {
setSignupDialogOpen(false); setSignupDialogOpen(false);
setLoginDialogOpen(true); setLoginDialogOpen(true);
}} }}
/> />
</>
)}
</div> </div>
); );
} }

View file

@ -321,7 +321,7 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
return ( return (
<Dialog open={isOpen} onOpenChange={onClose}> <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')}> <DialogHeader className={cn('px-5 pt-5 pb-3')}>
<DialogTitle className="text-center font-black uppercase tracking-wide text-lg"> <DialogTitle className="text-center font-black uppercase tracking-wide text-lg">
{t('settingsUi.loginDialog.welcomeTitle')} {t('settingsUi.loginDialog.welcomeTitle')}

View file

@ -375,7 +375,7 @@ const SignupDialog: React.FC<SignupDialogProps> = ({ isOpen, onClose, onComplete
return ( return (
<Dialog open={isOpen} onOpenChange={onClose}> <Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent <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')}> <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')}> <DialogTitle className={cn('font-semibold text-center text-base sm:text-lg')}>