fix(auth): keep signup alive across page cards; sanitize trip links

This commit is contained in:
SkateSpots 2026-10-28 00:05:28 +07:00
parent e267b79281
commit 13ebea685e
7 changed files with 57 additions and 21 deletions

View file

@ -119,7 +119,7 @@ export function LanguageToggle({ compact = false, inline = false, className, var
}
return (
<DropdownMenu>
<DropdownMenu onOpenChange={(open) => { if (!open) setQuery(''); }}>
<DropdownMenuTrigger asChild>
<Button
variant={variant}

View file

@ -21,6 +21,7 @@ import { clearTranslationMemory } from '@/lib/translation';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useTranslation } from '@/contexts/language';
import { useLoggedInAccounts } from '@/hooks/useLoggedInAccounts';
import { AuthDialogContext } from '@/contexts/authDialog';
import {
MapPin,
@ -585,6 +586,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
const mainRef = useRef<HTMLElement>(null);
return (
<AuthDialogContext.Provider value={{ openLogin: requestLogin, openSignup: requestSignup }}>
<div className="min-h-screen bg-black text-white">
{/* Skip-to-content — visually hidden until focused. A real <button> (not
href="#main") because HashRouter treats the hash AS the route, so an
@ -802,7 +804,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
<Settings className="h-5 w-5" />
</Button>
</Link>
<LoginArea className="max-w-60" onLoginRequest={requestLogin} onSignupRequest={requestSignup} />
<LoginArea className="max-w-60" />
</div>
</div>
</header>
@ -843,7 +845,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
/>
)}
<div className={cn(user && 'hidden')}>
<LoginArea compact className="flex flex-row gap-1" onLoginRequest={requestLogin} onSignupRequest={requestSignup} />
<LoginArea compact className="flex flex-row gap-1" />
</div>
<Sheet
@ -1118,7 +1120,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" onLoginRequest={requestLogin} onSignupRequest={requestSignup} />
<LoginArea className="w-full justify-center" />
</div>
</>
)}
@ -1296,5 +1298,6 @@ export function Layout({ children, noPadding }: LayoutProps) {
{/* Mirrors in-app activity to OS notifications — inert until opted in. */}
<OsNotificationBridge />
</div>
</AuthDialogContext.Provider>
);
}

View file

@ -17,6 +17,7 @@ import { useCreateSpotCollection } from '@/hooks/useSpotCollections';
import { useToast } from '@/hooks/useToast';
import { useTranslation } from '@/contexts/language';
import { cn } from '@/lib/utils';
import { sanitizeUrl } from '@/lib/sanitizeUrl';
const KIND_ICON = {
spot: MapPin,
@ -117,6 +118,7 @@ export function TripItineraryCard({ itinerary }: { itinerary: Itinerary }) {
<div className="space-y-1">
{day.stops.map((stop, i) => {
const Icon = KIND_ICON[stop.kind];
const stopUrl = sanitizeUrl(stop.url);
const inner = (
<div className="flex items-start gap-1.5">
<Icon className="h-2.5 w-2.5 mt-0.5 flex-shrink-0 text-[#ccff00]/70" />
@ -131,10 +133,10 @@ export function TripItineraryCard({ itinerary }: { itinerary: Itinerary }) {
</div>
</div>
);
return stop.url ? (
return stopUrl ? (
<a
key={i}
href={stop.url}
href={stopUrl}
target="_blank"
rel="noopener noreferrer"
className="block rounded px-1.5 py-1 hover:bg-[#ccff00]/5 transition-colors"
@ -158,17 +160,23 @@ export function TripItineraryCard({ itinerary }: { itinerary: Itinerary }) {
<Users className="h-2.5 w-2.5" /> {t('routeUi.localCrews')}
</div>
<div className="flex flex-wrap gap-1">
{itinerary.crews.map((c, i) => (
{itinerary.crews.map((c, i) => {
const crewUrl = sanitizeUrl(c.url);
const cls = 'text-[9px] px-1.5 py-0.5 rounded bg-white/5 text-gray-200 hover:bg-[#ccff00]/10 hover:text-[#ccff00] transition-colors';
return crewUrl ? (
<a
key={i}
href={c.url}
href={crewUrl}
target="_blank"
rel="noopener noreferrer"
className="text-[9px] px-1.5 py-0.5 rounded bg-white/5 text-gray-200 hover:bg-[#ccff00]/10 hover:text-[#ccff00] transition-colors"
className={cls}
>
👥 {c.name}
</a>
))}
) : (
<span key={i} className={cls}>👥 {c.name}</span>
);
})}
</div>
</div>
)}

View file

@ -10,6 +10,7 @@ import { useLoggedInAccounts } from '@/hooks/useLoggedInAccounts';
import { AccountSwitcher } from './AccountSwitcher';
import { cn } from '@/lib/utils';
import { useTranslation } from '@/contexts/language';
import { useAuthDialog } from '@/contexts/authDialog';
export interface LoginAreaProps {
className?: string;
@ -27,16 +28,19 @@ export interface LoginAreaProps {
export function LoginArea({ className, compact, onLoginRequest, onSignupRequest }: LoginAreaProps) {
const { currentUser } = useLoggedInAccounts();
const { t } = useTranslation();
const authDialog = useAuthDialog();
const [loginDialogOpen, setLoginDialogOpen] = useState(false);
const [signupDialogOpen, setSignupDialogOpen] = useState(false);
const globalAuth = !!onLoginRequest && !!onSignupRequest;
const globalAuth = !!authDialog || (!!onLoginRequest && !!onSignupRequest);
const openLogin = () => {
if (onLoginRequest) onLoginRequest();
if (authDialog) authDialog.openLogin();
else if (onLoginRequest) onLoginRequest();
else setLoginDialogOpen(true);
};
const openSignup = () => {
if (onSignupRequest) onSignupRequest();
if (authDialog) authDialog.openSignup();
else if (onSignupRequest) onSignupRequest();
else setSignupDialogOpen(true);
};
@ -57,6 +61,7 @@ export function LoginArea({ className, compact, onLoginRequest, onSignupRequest
size="icon"
className='h-9 w-9 rounded-full bg-[#ccff00] text-black hover:bg-[#ccff00]/90'
title={t('settingsUi.loginArea.signUp')}
aria-label={t('settingsUi.loginArea.signUp')}
>
<UserPlus className='w-4 h-4' />
</Button>
@ -66,6 +71,7 @@ export function LoginArea({ className, compact, onLoginRequest, onSignupRequest
size="icon"
className="h-9 w-9 rounded-full border border-gray-600 hover:border-[#ff0040]"
title={t('settingsUi.loginArea.logIn')}
aria-label={t('settingsUi.loginArea.logIn')}
>
<User className='w-4 h-4' />
</Button>

View file

@ -321,7 +321,7 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<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")}>
<DialogContent onEscapeKeyDown={onClose} 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,6 +375,7 @@ const SignupDialog: React.FC<SignupDialogProps> = ({ isOpen, onClose, onComplete
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent
onEscapeKeyDown={onClose}
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')}>

View file

@ -0,0 +1,18 @@
import { createContext, useContext } from 'react';
export interface AuthDialogActions {
openLogin: () => void;
openSignup: () => void;
}
/**
* App-wide auth dialog controls. `Layout` owns the dialogs so they survive the
* unmount of whichever `LoginArea` (or parent card/dialog) opened them — crucial
* because signup logs the user in immediately and many cards conditionally
* render their login prompt only while logged out.
*/
export const AuthDialogContext = createContext<AuthDialogActions | null>(null);
export function useAuthDialog(): AuthDialogActions | null {
return useContext(AuthDialogContext);
}